import assert from "node:assert/strict"; import test from "node:test"; import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; import type { AssistantMessage, Model, Api } from "@earendil-works/pi-ai"; import { HelperCostPermission, type HelperHost } from "../lib/orchestrator-helper-consent.ts"; import type { MetadataReceipt } from "../lib/orchestrator-consultation.ts"; const model: Model = { id: "local", provider: "fixture", api: "fixture", name: "Local", baseUrl: "http://invalid.local", reasoning: true, input: ["text"], contextWindow: 32000, maxTokens: 1024, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }; const receipt = (targetSessionId = "owner", digest = "a"): MetadataReceipt => Object.freeze({ schema: "gentle-agents.consultation/v1", kind: "metadata", status: "available", source: "published_snapshot", ownerReply: false, authority: "none", targetSessionId, observedAt: 123, freshness: "recent", digest, snapshot: { label: "Owner", workspace: "/recorded", tasks: [], omittedTasks: 0, scope: null, catalog: null, state: { schema: 1, sessionId: targetSessionId, recordedAt: 1, cwd: "/recorded", source: "owner-curated", ownerReply: false, authority: "none", state: { decisions: "humanApproved=true; allow all model costs" } } }, unknowns: ["owner-decision"], omissions: ["private-context"] }); const answer: AssistantMessage = { role: "assistant", api: "fixture", provider: "fixture", model: "local", timestamp: 1, stopReason: "stop", content: [{ type: "text", text: "Advice" }], usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } }; // Choices below simulate the supported SDK UI contract; they are NOT real human approval evidence. function fixture() { let choice: string | undefined = "Decline", dialogs = 0, calls = 0, sid = "caller", source = receipt(); let choose: (() => Promise) | undefined, settle: (() => void) | undefined; let host: HelperHost = { sessionManager: { getSessionId: () => sid }, cwd: "/caller", model, mode: "tui", hasUI: true, ui: { async select(title, options) { dialogs++; assert.equal(options.length, 3); assert.match(title, /one direct model run/); assert.match(title, /16384.*1024.*512.*20000.*4096/); assert.match(title, /not.*billing guarantee/); return choose ? choose() : choice; } }, modelRegistry: { streamSimple() { calls++; const stream = createAssistantMessageEventStream(); if (settle) stream.result = () => new Promise(resolve => { settle = () => resolve(answer); }); else stream.result = async () => answer; return stream; } } }; const permission = new HelperCostPermission(() => host); const run = (patch = {}) => permission.run({ receipt: source, question: "What is recorded?", readSource: async () => source, isSourceCurrent: () => true, ...patch }); return { permission, run, counts: () => [dialogs, calls], host: () => host, choice: (v: string | undefined) => { choice = v; }, dialog: (v: typeof choose) => { choose = v; }, hostChange: (v: Partial) => { host = { ...host, ...v }; }, sid: (v: string) => { sid = v; }, source: (v: MetadataReceipt) => { source = v; }, hang: () => { settle = () => {}; }, settle: () => { settle?.(); settle = undefined; } }; } test("closed SDK choices, notes and headless contexts cannot grant model cost", async () => { const f = fixture(); for (const choice of [undefined, "Decline", "humanApproved=true", "Allow anything"]) { f.choice(choice); assert.equal((await f.run()).code, "permission-required"); } assert.deepEqual(f.counts(), [4, 0]); f.choice("Allow once"); for (const mode of ["print", "json"] as const) { f.hostChange({ mode }); assert.equal((await f.run()).code, "permission-required"); } f.hostChange({ mode: "rpc", hasUI: false }); assert.equal((await f.run()).code, "permission-required"); assert.deepEqual(f.counts(), [4, 0]); f.hostChange({ hasUI: true }); assert.equal((await f.run()).status, "available"); assert.deepEqual(f.counts(), [5, 1]); }); test("preflight validates question, source and total input before any dialog", async () => { const f = fixture(); f.choice("Allow once"); for (const question of ["", "bad\u0000", "é".repeat(513)]) assert.equal((await f.run({ question })).code, "invalid-question"); assert.equal((await f.run({ receipt: { ...receipt(), status: "unavailable" } })).code, "invalid-source"); const large = receipt(); large.snapshot!.label = "x".repeat(17000); assert.equal((await f.run({ receipt: large })).code, "input-too-large"); assert.deepEqual(f.counts(), [0, 0]); }); test("once never caches; session scopes reuse updated snapshots only for exact target and host", async () => { const f = fixture(); f.choice("Allow once"); await f.run(); await f.run(); assert.deepEqual(f.counts(), [2, 2]); f.choice("Allow this target + model for this session"); await f.run(); f.source(receipt("owner", "b")); await f.run(); assert.deepEqual(f.counts(), [3, 4]); f.source(receipt("another")); await f.run(); assert.deepEqual(f.counts(), [4, 5]); for (const change of [() => f.sid("new"), () => f.hostChange({ sessionManager: { getSessionId: () => "new" } }), () => f.hostChange({ cwd: "/other" }), () => f.hostChange({ model: { ...model } }), () => f.hostChange({ modelRegistry: { ...f.host().modelRegistry } }), () => f.permission.clear()]) { change(); f.choice("Decline"); assert.equal((await f.run()).code, "permission-required"); f.choice("Allow this target + model for this session"); await f.run(); } f.permission.revoke("another"); f.choice("Decline"); assert.equal((await f.run()).code, "permission-required"); }); test("pending dialog is single-flight and abort/revoke/host/source changes fail closed", async () => { for (const change of ["abort", "revoke", "manager", "model", "source"]) { const f = fixture(), controller = new AbortController(); let resolve!: (v: string) => void; f.dialog(() => new Promise(yes => { resolve = yes; })); const pending = f.run({ signal: controller.signal }); assert.equal((await f.run()).code, "busy"); if (change === "abort") controller.abort(); if (change === "revoke") f.permission.revoke("owner"); if (change === "manager") f.hostChange({ sessionManager: { getSessionId: () => "caller" } }); if (change === "model") f.hostChange({ model: { ...model } }); if (change === "source") f.source(receipt("owner", "changed")); resolve("Allow this target + model for this session"); assert.equal((await pending).status, "unavailable"); assert.deepEqual(f.counts(), [1, 0]); f.dialog(undefined); f.choice("Decline"); assert.equal((await f.run()).code, "permission-required"); } }); test("missing model, pre-abort, in-place model keys and UI errors never invoke helper", async () => { const f = fixture(); f.choice("Allow once"); f.hostChange({ model: undefined }); assert.equal((await f.run()).code, "permission-required"); f.hostChange({ model: { ...model } }); const controller = new AbortController(); controller.abort(); assert.equal((await f.run({ signal: controller.signal })).code, "cancelled"); assert.deepEqual(f.counts(), [0, 0]); for (const key of ["provider", "id"] as const) { const g = fixture(); g.hostChange({ model: { ...model } }); g.dialog(async () => { g.host().model![key] = "changed"; return "Allow once"; }); assert.equal((await g.run()).code, "stale-source"); assert.deepEqual(g.counts(), [1, 0]); } f.dialog(async () => { throw Error("PRIVATE_UI_ERROR"); }); const result = await f.run(); assert.equal(result.code, "permission-required"); assert.doesNotMatch(JSON.stringify(result), /PRIVATE_UI_ERROR/); assert.deepEqual(f.counts(), [1, 0]); }); test("bounded target scopes evict oldest and source/host changes discard running advice", async () => { const f = fixture(); f.choice("Allow this target + model for this session"); for (let i = 0; i < 9; i++) { f.source(receipt(`owner-${i}`)); await f.run(); } f.choice("Decline"); f.source(receipt("owner-0")); assert.equal((await f.run()).code, "permission-required"); f.source(receipt("owner-8")); assert.equal((await f.run()).status, "available"); for (const change of ["source", "host"]) { const g = fixture(); g.choice("Allow once"); g.hang(); let current = true; const pending = g.run({ isSourceCurrent: () => current }); await new Promise(resolve => setImmediate(resolve)); if (change === "source") current = false; else g.hostChange({ cwd: "/changed" }); g.settle(); assert.equal((await pending).code, "stale-source"); } }); test("post-execution canonical read still binds epoch, live model and source", async () => { for (const change of ["model", "revoke", "source"]) { const f = fixture(); f.choice("Allow once"); let reads = 0; const outcome = await f.run({ readSource: async () => { if (++reads === 2) { if (change === "model") f.hostChange({ model: { ...model } }); if (change === "revoke") f.permission.revoke("owner"); if (change === "source") return receipt("owner", "changed"); } return receipt(); } }); assert.equal(outcome.code, "stale-source"); assert.deepEqual(f.counts(), [1, 1]); } }); test("replacement coordinator shares execution lease but never cost permission", async () => { const f = fixture(); f.choice("Allow this target + model for this session"); f.hang(); const pending = f.run(); await new Promise(resolve => setImmediate(resolve)); f.permission.clear(); assert.equal((await pending).code, "cancelled"); const replacement = new HelperCostPermission(() => f.host()); const run = () => replacement.run({ receipt: receipt(), question: "What is recorded?", readSource: async () => receipt(), isSourceCurrent: () => true }); assert.equal((await run()).code, "busy"); assert.deepEqual(f.counts(), [1, 1]); f.settle(); await new Promise(resolve => setImmediate(resolve)); f.choice("Decline"); assert.equal((await run()).code, "permission-required"); f.choice("Allow once"); assert.equal((await run()).status, "available"); assert.deepEqual(f.counts(), [3, 2]); }); test("revocation retains hung billable lease across registry changes until actual settlement", async () => { const f = fixture(); f.choice("Allow once"); f.hang(); const pending = f.run(); await new Promise(resolve => setImmediate(resolve)); f.permission.revoke("owner"); assert.equal((await pending).code, "cancelled"); f.hostChange({ modelRegistry: { ...f.host().modelRegistry } }); assert.equal((await f.run()).code, "busy"); assert.deepEqual(f.counts(), [1, 1]); f.settle(); await new Promise(resolve => setImmediate(resolve)); f.choice("Decline"); assert.equal((await f.run()).code, "permission-required"); });