Files
ThothII/harness/tests/fake_pi/fake_pi_rpc.mjs
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

52 lines
2.7 KiB
JavaScript

// harness/tests/fake_pi/fake_pi_rpc.mjs — scripted RPC test double (LF-only JSONL).
import fs from "node:fs";
import crypto from "node:crypto";
const script = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const out = (evt) => process.stdout.write(JSON.stringify(evt) + "\n");
// Mirror real Pi (rpc-mode createDialogPromise): ctx.ui.input assegna un id RPC PROPRIO
// (crypto.randomUUID), distinto dall'id interno del descriptor che viaggia opaco nel
// `title`. La risposta si correla su quell'id RPC; un id sconosciuto viene scartato in
// silenzio (esattamente cio' che provocava lo "stuck senza output" quando l'host
// rispondeva con l'id del descriptor invece dell'id RPC).
const pendingUi = new Map(); // piId (RPC) -> descriptor.id
let buf = "";
process.stdin.on("data", (chunk) => {
buf += chunk.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;
let cmd; try { cmd = JSON.parse(line); } catch { continue; }
if (cmd.type === "prompt") {
for (const step of script.on_prompt ?? []) {
if (step.ui_request_descriptor) {
const d = step.ui_request_descriptor;
const piId = crypto.randomUUID(); // id RPC proprio di Pi (≠ descriptor.id)
pendingUi.set(piId, d.id);
out({ type: "extension_ui_request", id: piId, method: "input", title: JSON.stringify(d) });
} else { out(step); } // eventi non-UI (text_delta, agent_end, …) passano tali e quali
}
} else if (cmd.type === "extension_ui_response") {
const descId = pendingUi.get(cmd.id); // correla SOLO sull'id RPC di Pi
if (descId !== undefined) {
pendingUi.delete(cmd.id);
for (const evt of (script.on_response ?? {})[descId] ?? []) out(evt);
}
} else if (cmd.type === "get_available_models") {
out({ type: "response", command: "get_available_models", id: cmd.id, success: true,
data: { models: script.available_models ?? [] } });
} else if (cmd.type === "steer") {
out({ type: "response", command: "steer", id: cmd.id, success: true });
} else if (cmd.type === "get_state") {
out({ type: "response", command: "get_state", id: cmd.id, success: true,
data: { sessionId: "fake", thinkingLevel: "medium", isStreaming: false } });
} else if (cmd.type === "set_model") {
out({ type: "response", command: "set_model", id: cmd.id, success: true });
} else if (cmd.type === "set_thinking_level") {
out({ type: "response", command: "set_thinking_level", id: cmd.id, success: true });
}
}
});
process.stdin.on("end", () => process.exit(0));