Files
ThothII/harness/tests/fake_pi/test_fake_pi_contract.mjs
T
marcopanandClaude Opus 4.8 418187a4ad 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>
2026-06-30 10:43:49 +02:00

60 lines
3.0 KiB
JavaScript

// harness/tests/fake_pi/test_fake_pi_contract.mjs — run: node --test
import test from "node:test";
import assert from "node:assert";
import { spawn } from "node:child_process";
import path from "node:path";
// Avvia il fake, manda il prompt e — quando arriva il widget — risponde con l'id RPC
// indicato da `respondWith` (una funzione widget -> id). Mirror del vero Pi: la risposta
// si correla sull'id RPC top-level, non sull'id interno del descriptor.
function driveReactive(scriptPath, respondWith) {
return new Promise((resolve) => {
const fp = spawn("node", [path.join(import.meta.dirname, "fake_pi_rpc.mjs"), scriptPath]);
const events = []; let buf = "";
fp.stdout.on("data", (c) => {
buf += c.toString("utf8");
for (let nl; (nl = buf.indexOf("\n")) !== -1; ) {
const line = buf.slice(0, nl).replace(/\r$/, ""); buf = buf.slice(nl + 1);
if (!line) continue;
const evt = JSON.parse(line);
events.push(evt);
if (evt.type === "extension_ui_request" && evt.method === "input") {
const desc = JSON.parse(evt.title);
fp.stdin.write(JSON.stringify({
type: "extension_ui_response",
id: respondWith(evt),
value: JSON.stringify({ id: desc.id, choices: ["a"], decision: { type: "concept_clarified" } }),
}) + "\n");
}
}
});
fp.on("exit", () => resolve(events));
fp.stdin.write(JSON.stringify({ type: "prompt", message: '/nuova-domanda "x"' }) + "\n");
setTimeout(() => fp.stdin.end(), 300);
});
}
test("on prompt emette il widget F1 con id RPC distinto dal descriptor; rispondere con l'id RPC sblocca", async () => {
const sp = path.join(import.meta.dirname, "scripts/f1_disambiguation.json");
// Host corretto: risponde con l'id RPC top-level del widget.
const events = await driveReactive(sp, (w) => w.id);
const widget = events.find((e) => e.type === "extension_ui_request");
assert.equal(widget.method, "input"); // shape nativa di Pi
const descriptor = JSON.parse(widget.title); // il descriptor viaggia nel title
assert.equal(descriptor.widget, "select");
assert.equal(descriptor.id, "u1");
assert.notEqual(widget.id, descriptor.id); // l'id RPC di Pi NON e' l'id del descriptor
// La risposta correlata sblocca il follow-up (testo + agent_end).
assert.ok(events.some((e) => e.type === "message_update"));
assert.ok(events.some((e) => e.type === "agent_end"));
});
test("rispondere con l'id del descriptor (sbagliato) viene scartato: nessun follow-up", async () => {
const sp = path.join(import.meta.dirname, "scripts/f1_disambiguation.json");
// Host bug: risponde con l'id interno del descriptor invece dell'id RPC -> drop.
const events = await driveReactive(sp, (w) => JSON.parse(w.title).id);
assert.ok(events.some((e) => e.type === "extension_ui_request"));
assert.ok(!events.some((e) => e.type === "agent_end")); // mai sbloccato
assert.ok(!events.some((e) => e.type === "message_update"));
});