ctx.ui.input in `pi --mode rpc` correlates extension_ui_response on its own
top-level RPC id (crypto.randomUUID), not the descriptor id the gate carries
in `title`. SessionBridge replied with the descriptor id, so Pi silently
dropped the response and the model never resumed — every reviewer widget hung
after the human answered.
SessionBridge now stores Pi's top-level m.id (pendingPiId) and replies
extension_ui_response{ id: pendingPiId, value: <uiResponse> }; value still
carries the descriptor id so the gate's internal resp.id === descriptor.id
check still holds.
The fake-pi double had masked the bug by forcing m.id == descriptor.id; it now
mirrors real Pi (distinct randomUUID, correlate on it, drop unknown ids), with
a negative regression test. SKILL.md Phase 1 also now steers multi-answer
disambiguation to reviewer_decide (multiselect).
Tests: backend 67/67, tsc clean, fake-pi contract 2/2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.0 KiB
TypeScript
77 lines
3.0 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" }), {
|
|
thtRunner: {
|
|
ollamaEnsure: async () => ({ ok: true }),
|
|
sessionNew: async () => ({ id: "s1" }),
|
|
sessionShow: async (_id: string) => ({ id: "s1", 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
|
|
await fetch(`${base}/sessions`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ workspace: "w", question: "q" }),
|
|
});
|
|
|
|
// 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);
|