fix(backend): echo Pi's RPC id so reviewer gates unblock after answer
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>
This commit is contained in:
@@ -7,7 +7,28 @@ 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");
|
||||
|
||||
test("loop F1: crea sessione → SSE riceve il widget → risponde → 204", async () => {
|
||||
// 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 }),
|
||||
@@ -30,15 +51,13 @@ test("loop F1: crea sessione → SSE riceve il widget → risponde → 204", asy
|
||||
// 2. Small delay to let fake-pi process the prompt and fill pendingWidget
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// 3. SSE: read the first event (pendingWidget re-emit via subscribe)
|
||||
// 3. SSE: the pending widget is re-emitted on subscribe
|
||||
const es = await fetch(`${base}/sessions/s1/events`);
|
||||
const reader = es.body!.getReader();
|
||||
const chunk = await reader.read();
|
||||
const text = new TextDecoder().decode(chunk.value);
|
||||
expect(text).toContain("ui_request");
|
||||
await reader.cancel();
|
||||
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
|
||||
// 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" },
|
||||
@@ -46,5 +65,12 @@ test("loop F1: crea sessione → SSE riceve il widget → risponde → 204", asy
|
||||
});
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user