diff --git a/harness/tests/fake_pi/fake_pi_rpc.mjs b/harness/tests/fake_pi/fake_pi_rpc.mjs new file mode 100644 index 00000000..43b8d21e --- /dev/null +++ b/harness/tests/fake_pi/fake_pi_rpc.mjs @@ -0,0 +1,33 @@ +// harness/tests/fake_pi/fake_pi_rpc.mjs — scripted RPC test double (LF-only JSONL). +import fs from "node:fs"; +const script = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +const out = (evt) => process.stdout.write(JSON.stringify(evt) + "\n"); + +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; + out({ type: "extension_ui_request", id: d.id, 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") { + for (const evt of (script.on_response ?? {})[cmd.id] ?? []) 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 } }); + } + } +}); +process.stdin.on("end", () => process.exit(0)); diff --git a/harness/tests/fake_pi/scripts/f1_disambiguation.json b/harness/tests/fake_pi/scripts/f1_disambiguation.json new file mode 100644 index 00000000..fe27e9cc --- /dev/null +++ b/harness/tests/fake_pi/scripts/f1_disambiguation.json @@ -0,0 +1,11 @@ +{ + "on_prompt": [ + { "ui_request_descriptor": { "type": "ui_request", "id": "u1", "schema_version": 1, + "phase": "F1_chiarimento", "title": "Disambigua", + "widget": "select", + "options": [ {"id":"a","label":"interpretazione A"}, {"id":"b","label":"interpretazione B"} ], + "reserved": ["back","exit","other"] } } + ], + "on_response": { "u1": [ { "type": "agent_end" } ] }, + "available_models": [ {"provider":"zai","id":"glm-5.2"} ] +} diff --git a/harness/tests/fake_pi/test_fake_pi_contract.mjs b/harness/tests/fake_pi/test_fake_pi_contract.mjs new file mode 100644 index 00000000..7a9e59de --- /dev/null +++ b/harness/tests/fake_pi/test_fake_pi_contract.mjs @@ -0,0 +1,38 @@ +// 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"; + +function drive(scriptPath, commands) { + 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) events.push(JSON.parse(line)); + } + }); + fp.on("exit", () => resolve(events)); + for (const cmd of commands) fp.stdin.write(JSON.stringify(cmd) + "\n"); + setTimeout(() => fp.stdin.end(), 300); + }); +} + +test("on prompt emette il widget F1; on response avanza", async () => { + const sp = path.join(import.meta.dirname, "scripts/f1_disambiguation.json"); + const events = await drive(sp, [ + { type: "prompt", message: "/nuova-domanda \"x\"" }, + { type: "extension_ui_response", id: "u1", + value: JSON.stringify({ id: "u1", choices: ["a"], decision: { type: "concept_clarified" } }) }, + ]); + const widget = events.find((e) => e.type === "extension_ui_request"); + assert.equal(widget.method, "input"); // shape nativa di Pi + assert.equal(widget.id, "u1"); + const descriptor = JSON.parse(widget.title); // il descriptor viaggia nel title + assert.equal(descriptor.widget, "select"); + assert.equal(descriptor.id, "u1"); + assert.ok(events.some((e) => e.type === "agent_end")); +});