Both run on pinix, published only through the pinix Tailnet namespace and Traefik's web-internal entrypoint (unbound local-data + Headscale split DNS for intent./ereader.domain2). Live Intent reads its OpenAI key from the new pinix-only agenix secret. ereaderViewer is a path: flake input for now; the repo split plan moves it into its own repository on cgit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.9 KiB
JavaScript
114 lines
4.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
import { createServer } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import { createApp, sessionConfiguration } from "../server.mjs";
|
|
|
|
const origin = "https://intent.example.test";
|
|
const conversationId = "2026-09-15-123e4567-e89b-12d3-a456-426614174000";
|
|
|
|
async function withApp(options, run) {
|
|
const server = createServer(createApp({ origin, apiKey: "test-key", ...options }));
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const { port } = server.address();
|
|
try {
|
|
await run(`http://127.0.0.1:${port}`);
|
|
} finally {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
}
|
|
}
|
|
|
|
function post(url, path, payload, headers = {}) {
|
|
return fetch(`${url}${path}`, {
|
|
method: "POST",
|
|
headers: { Origin: origin, "Content-Type": "application/json", ...headers },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
function payload(overrides = {}) {
|
|
return {
|
|
conversationId,
|
|
finalization: "confirmed",
|
|
transcript: [{ speaker: "Nutzer", text: "Bitte halte dieses Gespräch fest." }],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("creates a Live WebRTC session through the server only", async () => {
|
|
let received;
|
|
await withApp({
|
|
fetchImpl: async (_url, options) => {
|
|
received = JSON.parse(options.body);
|
|
return Response.json({ session: { id: "live_test" }, transport: { type: "webrtc", sdp: "answer" } }, { status: 201 });
|
|
},
|
|
}, async (url) => {
|
|
const response = await post(url, "/api/session", { sdp: "offer" });
|
|
assert.equal(response.status, 201);
|
|
assert.equal((await response.json()).session.id, "live_test");
|
|
});
|
|
assert.equal(received.session.model, "gpt-live-1");
|
|
assert.equal(received.transport.type, "webrtc");
|
|
assert.equal(received.session.store, false);
|
|
});
|
|
|
|
test("rejects cross-origin state-changing requests", async () => {
|
|
await withApp({}, async (url) => {
|
|
const response = await fetch(`${url}/api/transcripts`, {
|
|
method: "POST",
|
|
headers: { Origin: "https://wrong.example", "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload()),
|
|
});
|
|
assert.equal(response.status, 403);
|
|
});
|
|
});
|
|
|
|
test("saves a transcript without an API call, retries idempotently, and downloads it", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "live-intent-test-"));
|
|
let calls = 0;
|
|
await withApp({ apiKey: "", transcriptsDirectory: directory, fetchImpl: async () => { calls += 1; throw new Error("must not call API"); } }, async (url) => {
|
|
const first = await post(url, "/api/transcripts", payload());
|
|
assert.equal(first.status, 201);
|
|
const saved = await first.json();
|
|
assert.equal(saved.created, true);
|
|
const retry = await post(url, "/api/transcripts", payload());
|
|
assert.equal(retry.status, 200);
|
|
assert.equal((await retry.json()).created, false);
|
|
const stored = await fetch(`${url}${saved.url}`);
|
|
assert.equal(stored.status, 200);
|
|
assert.match(await stored.text(), /Session-Abschluss: bestätigt/);
|
|
});
|
|
assert.equal(calls, 0);
|
|
assert.match(await readFile(join(directory, conversationId, "transcript.md"), "utf8"), /Gesprächstranskript/);
|
|
});
|
|
|
|
test("rejects a collision without overwriting the original transcript", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "live-intent-test-"));
|
|
await withApp({ transcriptsDirectory: directory }, async (url) => {
|
|
assert.equal((await post(url, "/api/transcripts", payload({ finalization: "unconfirmed" }))).status, 201);
|
|
const collision = await post(url, "/api/transcripts", payload({ transcript: [{ speaker: "Nutzer", text: "Anderer Inhalt." }] }));
|
|
assert.equal(collision.status, 409);
|
|
});
|
|
assert.match(await readFile(join(directory, conversationId, "transcript.md"), "utf8"), /Session-Abschluss: unbestätigt/);
|
|
});
|
|
|
|
test("preserves existing intents while storing new transcripts", async () => {
|
|
const stateDirectory = await mkdtemp(join(tmpdir(), "live-intent-test-"));
|
|
const oldIntent = join(stateDirectory, "intents", "old-conversation", "intent.md");
|
|
await mkdir(join(stateDirectory, "intents", "old-conversation"), { recursive: true });
|
|
await writeFile(oldIntent, "# Alter Intent\n", "utf8");
|
|
await withApp({ transcriptsDirectory: join(stateDirectory, "transcripts") }, async (url) => {
|
|
assert.equal((await post(url, "/api/transcripts", payload())).status, 201);
|
|
});
|
|
assert.equal(await readFile(oldIntent, "utf8"), "# Alter Intent\n");
|
|
});
|
|
|
|
test("session configuration preserves the documented model and in-conversation delegation", () => {
|
|
const body = sessionConfiguration({ liveModel: "gpt-live-1", backendModel: "gpt-5.6-luna" }, "offer");
|
|
assert.equal(body.session.model, "gpt-live-1");
|
|
assert.equal(body.transport.type, "webrtc");
|
|
assert.equal(body.session.delegation.type, "responses");
|
|
assert.match(body.session.instructions, /kein Intent erzeugt/);
|
|
});
|