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.
81 lines
3.2 KiB
TypeScript
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);
|