Files
ThothII/backend/test/e2e-f1.test.ts
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

81 lines
3.2 KiB
TypeScript

import { test, expect } from "vitest";
import { spawn } from "node:child_process";
import path from "node:path";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
const FAKE = path.resolve("../harness/tests/fake_pi/fake_pi_rpc.mjs");
const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.json");
// Legge dallo stream SSE finche' `predicate(acc)` e' vero o scade il timeout.
async function readUntil(
reader: ReadableStreamDefaultReader<Uint8Array>,
predicate: (acc: string) => boolean,
timeoutMs: number,
): Promise<string> {
const dec = new TextDecoder();
let acc = "";
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res: any = await Promise.race([
reader.read(),
new Promise((r) => setTimeout(() => r({ timeout: true }), deadline - Date.now())),
]);
if (res.timeout || res.done) break;
acc += dec.decode(res.value);
if (predicate(acc)) return acc;
}
return acc;
}
test("loop F1: crea sessione → SSE riceve il widget → risponde → il modello riparte (follow-up)", async () => {
const app = buildApp(loadConfig({
THT_HARNESS_DIR: "../harness", THT_LEGACY_WORKSPACE_MODE: "local",
}), {
thtRunner: {
ollamaEnsure: async () => ({ ok: true }),
searchPack: async () => {},
sessionNew: async () => ({ id: "s1" }),
sessionShow: async (_id: string) => ({ id: "s1", interaction_language: "en", provider: undefined, model: undefined, thinking: undefined }),
sessionList: async () => [],
} as any,
spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any,
});
await app.listen({ port: 0, host: "127.0.0.1" });
const base = `http://127.0.0.1:${(app.server.address() as any).port}`;
// 1. Create session — spawns fake-pi, sends prompt, fake-pi emits widget
const created = await fetch(`${base}/sessions`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ workspace: "w", question: "q", interactionLanguage: "en" }),
});
expect(created.status).toBe(200);
// 2. Small delay to let fake-pi process the prompt and fill pendingWidget
await new Promise((r) => setTimeout(r, 50));
// 3. SSE: the pending widget is re-emitted on subscribe
const es = await fetch(`${base}/sessions/s1/events`);
const reader = es.body!.getReader();
const first = await readUntil(reader, (t) => t.includes("ui_request"), 2000);
expect(first).toContain("ui_request");
// 4. POST response — widget id is "u1" from f1_disambiguation.json (id INTERNO del descriptor)
const resp = await fetch(`${base}/sessions/s1/response`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ui_response: { id: "u1", choices: ["a"] } }),
});
expect(resp.status).toBe(204);
// 5. Prova del fix: la risposta deve essere correlata sull'id RPC di Pi, cosi' ctx.ui.input
// si risolve e il modello produce il follow-up. Col bug, la risposta veniva scartata e
// nessun follow-up arrivava ("stuck senza output").
const after = await readUntil(reader, (t) => t.includes("Procedo."), 2000);
expect(after).toContain("Procedo.");
await reader.cancel();
await app.close();
}, 10000);