From f850c391d86e5ab5a67be94de863c7eedaad9c4e Mon Sep 17 00:00:00 2001 From: mptyl Date: Sat, 27 Jun 2026 19:48:04 +0200 Subject: [PATCH] test(harness): fake-pi-runtime mock for gate CI tests --- .../gate/__tests__/fake_pi_runtime.js | 34 +++++++++++++++++++ .../gate/__tests__/fake_pi_runtime.test.js | 24 +++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js create mode 100644 harness/.pi/extensions/gate/__tests__/fake_pi_runtime.test.js diff --git a/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js b/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js new file mode 100644 index 00000000..c5268da0 --- /dev/null +++ b/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js @@ -0,0 +1,34 @@ +// fake_pi_runtime.js — minimal mock of the Pi extension runtime for gate tests. +// Models Pi's NATIVE extension UI API (ctx.ui.select/confirm/input). There is NO +// ctx.sendRaw in Pi; the gate awaits ctx.ui.* and the runtime routes the response. +function createFakePi() { + const handlers = new Map(); + const tools = new Map(); + const ctx = { + hasUI: true, + cwd: "/tmp/fake-pi-session", + notifications: [], + uiCalls: [], + uiQueue: [], + ui: { + notify: async (message, level = "info") => ctx.notifications.push({ message, level }), + input: async (title, placeholder, opts) => { ctx.uiCalls.push({ method: "input", title, placeholder, opts }); return ctx.uiQueue.shift(); }, + select: async (title, options, opts) => { ctx.uiCalls.push({ method: "select", title, options, opts }); return ctx.uiQueue.shift(); }, + confirm: async (title, message, opts) => { ctx.uiCalls.push({ method: "confirm", title, message, opts }); return ctx.uiQueue.shift(); }, + }, + }; + const pi = { + on: (event, handler) => { + if (!handlers.has(event)) handlers.set(event, []); + handlers.get(event).push(handler); + }, + registerTool: (def, fn) => tools.set(def?.name ?? def, { def, fn }), + emit: async (event, payload) => { + let result; + for (const h of handlers.get(event) ?? []) result = await h(payload, ctx); + return result; + }, + }; + return { pi, ctx, tools, emit: pi.emit, enqueueUi: (v) => ctx.uiQueue.push(v) }; +} +module.exports = { createFakePi }; diff --git a/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.test.js b/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.test.js new file mode 100644 index 00000000..84335f9e --- /dev/null +++ b/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.test.js @@ -0,0 +1,24 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const { createFakePi } = require("./fake_pi_runtime.js"); + +test("pi.on + emit invoca il handler e ne ritorna il valore", async () => { + const { pi } = createFakePi(); + pi.on("tool_call", (e) => (e.toolName === "x" ? { block: true } : undefined)); + assert.deepEqual(await pi.emit("tool_call", { toolName: "x" }), { block: true }); + assert.equal(await pi.emit("tool_call", { toolName: "y" }), undefined); +}); + +test("ctx.ui.input registra la chiamata e ritorna il valore in coda (API nativa)", async () => { + const { ctx, enqueueUi } = createFakePi(); + enqueueUi('{"id":"u1","choices":["a"]}'); + const v = await ctx.ui.input("title-json", ""); + assert.equal(v, '{"id":"u1","choices":["a"]}'); + assert.equal(ctx.uiCalls[0].method, "input"); + assert.equal(ctx.uiCalls[0].title, "title-json"); +}); + +test("ctx.ui.input senza valore in coda ritorna undefined (modella cancel)", async () => { + const { ctx } = createFakePi(); + assert.equal(await ctx.ui.input("t", ""), undefined); +});