diff --git a/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js b/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js index c5268da0..a4c037b5 100644 --- a/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js +++ b/harness/.pi/extensions/gate/__tests__/fake_pi_runtime.js @@ -17,18 +17,20 @@ function createFakePi() { confirm: async (title, message, opts) => { ctx.uiCalls.push({ method: "confirm", title, message, opts }); return ctx.uiQueue.shift(); }, }, }; + const commands = new Map(); 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 }), + registerCommand: (name, def) => commands.set(name, def), 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) }; + return { pi, ctx, tools, commands, emit: pi.emit, enqueueUi: (v) => ctx.uiQueue.push(v) }; } module.exports = { createFakePi }; diff --git a/harness/.pi/extensions/gate/__tests__/gate_entry.test.js b/harness/.pi/extensions/gate/__tests__/gate_entry.test.js new file mode 100644 index 00000000..7a2e71d8 --- /dev/null +++ b/harness/.pi/extensions/gate/__tests__/gate_entry.test.js @@ -0,0 +1,27 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const { createFakePi } = require("./fake_pi_runtime.js"); + +// The gate is ESM; resolve it once via dynamic import (Global Constraints: use await import() +// if require() of ESM misbehaves on this Node version). +const installGatePromise = import("../../tht-gate.js").then((m) => m.default); + +test("avvio workflow via input non-interattivo attiva il lock (free text bloccato)", async () => { + const installGate = await installGatePromise; + const { pi } = createFakePi(); + installGate(pi); + // entry del workflow con source 'rpc' (come un comando prompt RPC) + await pi.emit("input", { source: "rpc", text: '/nuova-domanda "x"' }); + // dopo l'entry, un testo libero senza '!' deve essere bloccato (lock attivo) + const res = await pi.emit("input", { source: "rpc", text: "promuovi la tabella pazienti" }); + assert.equal(res.action, "handled"); +}); + +test("testo con '!' passa al modello (steer) anche con lock attivo", async () => { + const installGate = await installGatePromise; + const { pi } = createFakePi(); + installGate(pi); + await pi.emit("input", { source: "rpc", text: '/nuova-domanda "x"' }); + const res = await pi.emit("input", { source: "rpc", text: "!considera solo il 2024" }); + assert.deepEqual(res, { action: "transform", text: "considera solo il 2024" }); +}); diff --git a/harness/.pi/extensions/tht-gate.js b/harness/.pi/extensions/tht-gate.js index f6d54838..c190472e 100644 --- a/harness/.pi/extensions/tht-gate.js +++ b/harness/.pi/extensions/tht-gate.js @@ -217,10 +217,8 @@ export default function (pi) { // 2) INPUT hook: workflow entry detection + free-input block + `!` steer channel. pi.on("input", async (event, ctx) => { const raw = (event.text ?? "").trimStart(); - if ( - event.source === "interactive" && - /^\/(nuova-domanda|riprendi-sessione)\b/.test(raw) - ) { + // entry detection: workflow-start funziona sia da TUI sia da comando RPC `prompt`. + if (/^\/(nuova-domanda|riprendi-sessione)\b/.test(raw)) { // NOTE: the source runs an ollama preflight here; ThothII defers embeddings // readiness to the session's first vector op. Entry detection only: lockActive = true; @@ -229,7 +227,8 @@ export default function (pi) { ? NUOVA_DOMANDA_KICKOFF : RIPRENDI_KICKOFF; } - if (!lockActive || event.source !== "interactive") return { action: "continue" }; + // free-input block: attivo quando il lock รจ su, per qualsiasi input utente (non solo interattivo). + if (!lockActive) return { action: "continue" }; const trimmed = (event.text ?? "").trimStart(); if (trimmed.length === 0) return { action: "continue" }; if (trimmed.startsWith("/")) return { action: "continue" }; diff --git a/harness/package-lock.json b/harness/package-lock.json new file mode 100644 index 00000000..78474fc8 --- /dev/null +++ b/harness/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "thothii-harness-gate", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "thothii-harness-gate", + "version": "0.1.0", + "devDependencies": { + "typebox": "1.1.38" + } + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/harness/package.json b/harness/package.json index 2d3df32c..105174b1 100644 --- a/harness/package.json +++ b/harness/package.json @@ -5,5 +5,8 @@ "description": "Pi gate extension for the ThothII NL->SQL harness (widget-descriptor builders + glue)", "scripts": { "test": "node --test .pi/extensions/gate/__tests__/*.test.js" + }, + "devDependencies": { + "typebox": "1.1.38" } }