import { test, expect, vi } from "vitest"; import { spawn } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { EventEmitter } from "node:events"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { PiProcessManager } from "../src/pi/pi-process-manager.js"; import { loadConfig } from "../src/config.js"; import { PI_MANAGED_CONFIG_ERROR_MESSAGE, validateDeclarativePiConfig, } from "../src/pi/managed-config.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FAKE = path.resolve(__dirname, "../../harness/tests/fake_pi/fake_pi_rpc.mjs"); const SCRIPT = path.resolve(__dirname, "../../harness/tests/fake_pi/scripts/f1_disambiguation.json"); const SAFE_AUTH = '{"deepseek":{"type":"api_key","key":"safe-token"}}\n'; const SAFE_MODELS = [ "{", ' "providers": {', ' "local-qwen": {"baseUrl":"http://model.invalid/v1","models":[{"id":"qwen"}]}', " }", "}", "", ].join("\n"); function writeSafeAgentConfig(agentDir: string): void { writeFileSync(path.join(agentDir, "auth.json"), SAFE_AUTH, { mode: 0o600 }); writeFileSync(path.join(agentDir, "models.json"), SAFE_MODELS, { mode: 0o600 }); } test("spawnFor avvia un runtime e il bridge emette il widget F1", async () => { const cfg = loadConfig({ THT_HARNESS_DIR: "../harness" }); const mgr = new PiProcessManager(cfg, { spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any }); const rt = await mgr.spawnFor("2026-06-27-100000-x", {}); const widget = await new Promise((res) => rt.bridge.onClientEvent((e) => e.type === "ui_request" && res(e))); expect(widget.ui_request.widget).toBe("select"); mgr.teardown("2026-06-27-100000-x"); expect(mgr.count()).toBe(0); }); test("l'exit del child rimuove il runtime dalla mappa (exit handler)", async () => { const cfg = loadConfig({ THT_HARNESS_DIR: "../harness" }); const mgr = new PiProcessManager(cfg, { spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any }); const rt = await mgr.spawnFor("exit-test", {}); expect(mgr.count()).toBe(1); // Cause the child to exit on its own and await the 'exit' event (no teardown call). const exited = new Promise((res) => rt.child.on("exit", () => res())); rt.child.kill(); await exited; // Let the manager's registered exit handler run. await new Promise((res) => setImmediate(res)); expect(mgr.count()).toBe(0); expect(mgr.get("exit-test")).toBeUndefined(); }); test("spawnFor sullo STESSO id rifiuta il duplicato senza interrompere il runtime attivo", async () => { const cfg = loadConfig({ THT_HARNESS_DIR: "../harness" }); const mgr = new PiProcessManager(cfg, { spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any }); const first = await mgr.spawnFor("dup-id", {}); expect(mgr.count()).toBe(1); await expect(mgr.spawnFor("dup-id", {})).rejects.toThrow( "session runtime already active: dup-id", ); expect(mgr.count()).toBe(1); expect(mgr.get("dup-id")).toBe(first); mgr.teardown("dup-id"); }); test("l'exit del VECCHIO child non elimina il nuovo runtime (exit identity-checked)", async () => { const cfg = loadConfig({ THT_HARNESS_DIR: "../harness" }); const firstChild = recordingChild(); const secondChild = recordingChild(); const children = [firstChild, secondChild]; const mgr = new PiProcessManager(cfg, { spawnFn: () => children.shift() as any }); const first = mgr.createFor("respawn-id", {}); mgr.teardown("respawn-id"); const second = mgr.createFor("respawn-id", {}); // The old child's exit handler fires after the respawn; it must NOT evict `second`. firstChild.emit("exit", 0); expect(mgr.get("respawn-id")).toBe(second); expect(mgr.count()).toBe(1); void first; mgr.teardown("respawn-id"); }); test("identity-checked teardown cannot kill a replacement runtime", () => { const cfg = loadConfig({ THT_HARNESS_DIR: "../harness" }); const firstChild = recordingChild(); const secondChild = recordingChild(); firstChild.kill = vi.fn(); secondChild.kill = vi.fn(); const children = [firstChild, secondChild]; const mgr = new PiProcessManager(cfg, { spawnFn: () => children.shift() as any }); const first = mgr.createFor("replace-id", {}); mgr.teardown("replace-id"); const second = mgr.createFor("replace-id", {}); expect(mgr.teardownIfCurrent("replace-id", first)).toBe(false); expect(mgr.get("replace-id")).toBe(second); expect(secondChild.kill).not.toHaveBeenCalled(); expect(mgr.teardownIfCurrent("replace-id", second)).toBe(true); expect(mgr.get("replace-id")).toBeUndefined(); expect(secondChild.kill).toHaveBeenCalledOnce(); }); test("oltre maxPiProcesses solleva errore", async () => { const cfg = { ...loadConfig({}), maxPiProcesses: 1 }; const mgr = new PiProcessManager(cfg, { spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any }); await mgr.spawnFor("a", {}); await expect(mgr.spawnFor("b", {})).rejects.toThrow(/max/i); mgr.teardown("a"); }); test("teardownForPrincipal stops only runtimes owned by that user", () => { const aliceChild = recordingChild(); const bobChild = recordingChild(); aliceChild.kill = vi.fn(); bobChild.kill = vi.fn(); const children = [aliceChild, bobChild]; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: () => children.shift() as any }); const alice = { issuer: "portal", subject: "alice", roles: ["user"] as const, permissions: ["session.use"] as const, isAdmin: false }; const bob = { issuer: "portal", subject: "bob", roles: ["user"] as const, permissions: ["session.use"] as const, isAdmin: false }; mgr.createFor("alice-session", { principal: alice }); mgr.createFor("bob-session", { principal: bob }); expect(mgr.teardownForPrincipal(alice)).toEqual(["alice-session"]); expect(mgr.get("alice-session")).toBeUndefined(); expect(mgr.get("bob-session")).toBeDefined(); expect(aliceChild.kill).toHaveBeenCalledOnce(); expect(bobChild.kill).not.toHaveBeenCalled(); mgr.teardown("bob-session"); }); test("createFor keeps at most one runtime for the same user", () => { const firstChild = recordingChild(); const secondChild = recordingChild(); firstChild.kill = vi.fn(); secondChild.kill = vi.fn(); const children = [firstChild, secondChild]; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: () => children.shift() as any }); const principal = { issuer: "portal", subject: "alice", roles: ["user"] as const, permissions: ["session.use"] as const, isAdmin: false }; mgr.createFor("first", { principal }); const second = mgr.createFor("second", { principal }); expect(mgr.count()).toBe(1); expect(mgr.get("first")).toBeUndefined(); expect(mgr.get("second")).toBe(second); expect(firstChild.kill).toHaveBeenCalledOnce(); expect(secondChild.kill).not.toHaveBeenCalled(); mgr.teardown("second"); }); function recordingChild() { const ch: any = new EventEmitter(); ch.stdout = new EventEmitter(); ch.stderr = new EventEmitter(); ch._writes = [] as string[]; ch.stdin = { write: (d: any) => { ch._writes.push(String(d)); return true; } }; ch.kill = () => {}; return ch; } test("Pi receives the leased workspace runtime config and releases it on direct teardown", () => { const child = recordingChild(); let spawnEnv: NodeJS.ProcessEnv | undefined; const release = vi.fn(); const mgr = new PiProcessManager(loadConfig({}), { spawnFn: (_command, _args, options) => { spawnEnv = options.env; return child as any; }, }); mgr.createFor("canonical-runtime", { runtimeConfig: { path: "/trusted/runtime-uuid.yaml", release }, } as any); expect(spawnEnv?.THT_CONFIG).toBe("/trusted/runtime-uuid.yaml"); // The child deliberately emits neither exit nor close. Ownership cleanup must not depend on it. mgr.teardown("canonical-runtime"); expect(release).toHaveBeenCalledOnce(); }); test("a close-only child event releases its temporary Pi agent snapshot", () => { const child = recordingChild(); let snapshotDir: string | undefined; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: (_command, _args, options) => { snapshotDir = options.env.PI_CODING_AGENT_DIR; return child as any; }, }); mgr.createFor("close-only-snapshot", {}); expect(snapshotDir).toBeTruthy(); expect(existsSync(snapshotDir!)).toBe(true); child.emit("close", 0); expect(existsSync(snapshotDir!)).toBe(false); mgr.teardown("close-only-snapshot"); }); test.each([ ["new", "auth.json", '{"deepseek":{"key":"!runtime-auth-command runtime-secret /private/runtime-auth"}}\n'], ["new", "models.json", '{"providers":{"local-qwen":{"headers":["!runtime-model-command runtime-secret /private/runtime-model"]}}}\n'], ["resume", "auth.json", '{"deepseek":{"key":"!resume-auth-command runtime-secret /private/resume-auth"}}\n'], ["resume", "models.json", '{"providers":{"local-qwen":{"models":[{"apiKey":"!resume-model-command runtime-secret /private/resume-model"}]}}}\n'], ] as const)( "%s runtime rejects post-admission executable %s before auth resolution or child spawn", async (mode, changedFile, unsafeRaw) => { const root = mkdtempSync(path.join(tmpdir(), "tht-runtime-managed-config-")); const agentDir = path.join(root, "agent"); mkdirSync(agentDir, { mode: 0o700 }); writeSafeAgentConfig(agentDir); vi.stubEnv("PI_CODING_AGENT_DIR", agentDir); let authResolutions = 0; let spawns = 0; const mgr = new PiProcessManager(loadConfig({}), { authProviders: () => { authResolutions += 1; return new Set(); }, spawnFn: () => { spawns += 1; throw new Error("SPAWN_BOUNDARY_REACHED"); }, }); try { // Admission/model validation succeeded while the mounted files were still safe, and the // session was then persisted. The operator-controlled mount changes before runtime start. validateDeclarativePiConfig(readFileSync(path.join(agentDir, "auth.json"), "utf8")); validateDeclarativePiConfig(readFileSync(path.join(agentDir, "models.json"), "utf8")); writeFileSync(path.join(root, "session-created"), `${mode}\n`); writeFileSync(path.join(agentDir, changedFile), unsafeRaw, { mode: 0o600 }); let failure: unknown; try { if (mode === "new") { mgr.createFor("post-admission-new", { provider: "local-qwen" }); } else { await mgr.spawnFor("post-admission-resume", { provider: "local-qwen", mode: "resume", }); } } catch (error) { failure = error; } const message = failure instanceof Error ? failure.message : String(failure); expect({ message, authResolutions, spawns, runtimes: mgr.count() }).toEqual({ message: PI_MANAGED_CONFIG_ERROR_MESSAGE, authResolutions: 0, spawns: 0, runtimes: 0, }); expect(message).not.toMatch(/runtime-secret|\/private\/|runtime-(?:auth|model)-command|resume-(?:auth|model)-command/); } finally { mgr.teardown("post-admission-new"); mgr.teardown("post-admission-resume"); vi.unstubAllEnvs(); rmSync(root, { recursive: true, force: true }); } }, ); test("runtime Pi consumes exact validated auth/models snapshots and keeps persistent agent resources", async () => { const root = mkdtempSync(path.join(tmpdir(), "tht-runtime-agent-snapshot-")); const agentDir = path.join(root, "agent"); const sessionsDir = path.join(agentDir, "sessions"); const extensionDir = path.join(agentDir, "extensions"); mkdirSync(sessionsDir, { recursive: true, mode: 0o700 }); mkdirSync(extensionDir, { recursive: true, mode: 0o700 }); writeSafeAgentConfig(agentDir); const settings = '{"quietStartup":true}\n'; writeFileSync(path.join(agentDir, "settings.json"), settings, { mode: 0o600 }); writeFileSync(path.join(extensionDir, "runtime-extension.js"), "export default {};\n"); vi.stubEnv("PI_CODING_AGENT_DIR", agentDir); vi.stubEnv("PI_CODING_AGENT_SESSION_DIR", ""); const child = recordingChild(); let spawnEnv: NodeJS.ProcessEnv | undefined; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: (_command, _args, options) => { spawnEnv = options.env; // This mutation happens after validation but before the child can open either source file. writeFileSync(path.join(agentDir, "auth.json"), '{"deepseek":{"key":"!late-auth-command"}}\n'); writeFileSync(path.join(agentDir, "models.json"), '{"providers":{"late":{"apiKey":"!late-model-command"}}}\n'); return child as any; }, }); let snapshotDir: string | undefined; let childExited = false; try { await mgr.spawnFor("snapshot-session", { provider: "local-qwen" }); snapshotDir = spawnEnv?.PI_CODING_AGENT_DIR; expect(snapshotDir).toBeTruthy(); expect(snapshotDir).not.toBe(agentDir); expect(readFileSync(path.join(snapshotDir!, "auth.json"), "utf8")).toBe(SAFE_AUTH); expect(readFileSync(path.join(snapshotDir!, "models.json"), "utf8")).toBe(SAFE_MODELS); expect(readFileSync(path.join(snapshotDir!, "settings.json"), "utf8")).toBe(settings); expect(readFileSync(path.join(snapshotDir!, "extensions", "runtime-extension.js"), "utf8")) .toBe("export default {};\n"); expect(spawnEnv?.PI_CODING_AGENT_SESSION_DIR).toBe(sessionsDir); mgr.teardown("snapshot-session"); child.emit("exit", 0); childExited = true; expect(existsSync(snapshotDir!)).toBe(false); } finally { mgr.teardown("snapshot-session"); if (!childExited) child.emit("exit", 0); vi.unstubAllEnvs(); if (snapshotDir && snapshotDir !== agentDir) rmSync(snapshotDir, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true }); } }); test("createFor kills a spawned child when post-spawn initialization throws", () => { const child = recordingChild(); child.kill = vi.fn(); child.stdout = { on: () => { throw new Error("READER_INIT_SENTINEL"); }, }; let snapshotDir: string | undefined; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: (_command, _args, options) => { snapshotDir = options.env.PI_CODING_AGENT_DIR; return child as any; }, }); expect(() => mgr.createFor("broken-init", {})).toThrow("READER_INIT_SENTINEL"); expect(snapshotDir).toBeTruthy(); expect(existsSync(snapshotDir!)).toBe(false); expect(child.kill).toHaveBeenCalledOnce(); expect(mgr.get("broken-init")).toBeUndefined(); expect(mgr.count()).toBe(0); }); test("createFor kills a spawned child when spawn boundary initialization throws", () => { const child = recordingChild(); child.kill = vi.fn(); child.stderr = { on: () => { throw new Error("STDERR_INIT_SENTINEL"); }, }; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: () => child as any }); expect(() => mgr.createFor("broken-spawn-init", {})).toThrow("STDERR_INIT_SENTINEL"); expect(child.kill).toHaveBeenCalledOnce(); expect(mgr.get("broken-spawn-init")).toBeUndefined(); expect(mgr.count()).toBe(0); }); test("un exit INATTESO del child notifica il client (info error + agent_end)", async () => { const cfg = loadConfig({}); const child = recordingChild(); const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any }); const rt = await mgr.spawnFor("crash-id", {}); const seen: any[] = []; rt.bridge.onClientEvent((e) => seen.push(e)); child.emit("exit", 137); expect(rt.bridge.turnState()).toBe("failed"); expect(seen).toEqual([ { type: "info", level: "error", text: expect.stringContaining("137") }, { type: "system_event", event: "session_failed" }, { type: "system_event", event: "agent_end" }, ]); expect(mgr.count()).toBe(0); }); test("l'exit dopo teardown NON emette eventi al client (uscita attesa)", async () => { const cfg = loadConfig({}); const child = recordingChild(); const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any }); const rt = await mgr.spawnFor("stop-id", {}); const seen: any[] = []; rt.bridge.onClientEvent((e) => seen.push(e)); mgr.teardown("stop-id"); child.emit("exit", 0); expect(seen).toEqual([]); }); test("spawnFor resume mode sends /riprendi-sessione ", async () => { const cfg = loadConfig({}); // no provider/model/thinking -> no rpc.request handshakes const child = recordingChild(); const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any }); await mgr.spawnFor("sid-9", { mode: "resume" }); expect(child._writes.join("")).toContain("/riprendi-sessione sid-9"); expect(child._writes.join("")).not.toContain("/nuova-domanda"); mgr.teardown("sid-9"); }); test("spawnFor default (new) mode sends /nuova-domanda", async () => { const cfg = loadConfig({}); const child = recordingChild(); const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any }); await mgr.spawnFor("sid-10", {}); expect(child._writes.join("")).toContain("/nuova-domanda"); mgr.teardown("sid-10"); }); test("spawnFor new mode forwards the real question instead of kickoff", async () => { const cfg = loadConfig({}); const child = recordingChild(); const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any }); await mgr.spawnFor("sid-question", { question: "pazienti con cardioversione e ILR" }); const prompt = JSON.parse(child._writes.at(-1)!); expect(prompt.message).toBe('/nuova-domanda "pazienti con cardioversione e ILR"'); expect(prompt.message).not.toContain("kickoff"); mgr.teardown("sid-question"); }); test("createFor does not prompt until start is called", async () => { const cfg = loadConfig({}); const child = recordingChild(); const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any }); const rt = mgr.createFor("sid-deferred", { question: "q" }); expect(child._writes).toEqual([]); await mgr.configure(rt, {}); expect(child._writes).toEqual([]); mgr.start("sid-deferred", rt, { question: "q" }); expect(JSON.parse(child._writes.at(-1)!).message).toBe('/nuova-domanda "q"'); mgr.teardown("sid-deferred"); }); test("configure gives the bridge the context window returned by set_model", async () => { const child = recordingChild(); child.stdin.write = (data: unknown) => { const request = JSON.parse(String(data)); child._writes.push(String(data)); if (request.type === "set_model") { queueMicrotask(() => child.stdout.emit("data", `${JSON.stringify({ type: "response", id: request.id, success: true, data: { contextWindow: 200_000 }, })}\n`)); } return true; }; const mgr = new PiProcessManager(loadConfig({}), { spawnFn: () => child as any }); const rt = mgr.createFor("context-window", {}); const seen: any[] = []; rt.bridge.onClientEvent((event) => seen.push(event)); await mgr.configure(rt, { provider: "zai", model: "glm-5.2" }); child.stdout.emit("data", `${JSON.stringify({ type: "message_end", message: { role: "assistant", stopReason: "stop", usage: { input: 10, cacheRead: 20, output: 5, totalTokens: 35 }, }, })}\n`); expect(seen).toContainEqual({ type: "usage", usage: { input: 10, cacheRead: 20, output: 5, totalTokens: 35, contextWindow: 200_000 }, }); mgr.teardown("context-window"); }); test("a created runtime is active during configure/bootstrap", () => { const child = recordingChild(); const mgr = new PiProcessManager(loadConfig({}), { spawnFn: () => child as any }); const runtime = mgr.createFor("starting-id", {}); expect(runtime.bridge.turnState()).toBe("running"); mgr.teardown("starting-id"); }); test("start reactivates the runtime before sending the prompt", () => { const child = recordingChild(); const mgr = new PiProcessManager(loadConfig({}), { spawnFn: () => child as any }); const runtime = mgr.createFor("restart-id", {}); child.stdout.emit("data", `${JSON.stringify({ type: "agent_end", messages: [] })}\n`); expect(runtime.bridge.turnState()).toBe("idle"); let stateAtWrite = runtime.bridge.turnState(); child.stdin.write = (data: unknown) => { stateAtWrite = runtime.bridge.turnState(); child._writes.push(String(data)); return true; }; mgr.start("restart-id", runtime, { question: "q" }); expect(stateAtWrite).toBe("running"); expect(runtime.bridge.turnState()).toBe("running"); mgr.teardown("restart-id"); }); test("production spawn uses explicit Pi path and passes portable data root without rewriting PATH", async () => { vi.stubEnv("PATH", "/usr/local/bin:/usr/bin"); vi.stubEnv("PI_PROVIDER_API_KEY", "provider-secret"); vi.stubEnv("NODE_EXTRA_CA_CERTS", "/certs/company-ca.pem"); const calls: any[][] = []; const child = recordingChild(); child.stderr.resume = () => {}; const spawnFn = (...args: any[]) => { calls.push(args); return child as any; }; const cfg = loadConfig({ THT_HARNESS_DIR: "/app/harness", PI_BIN: "/usr/local/bin/pi", THT_DATA_ROOT: "/data", }); const mgr = new PiProcessManager(cfg, { spawnFn }); try { await mgr.spawnFor("portable-session", { author: "user@example.test" }); const [bin, args, options] = calls[0]; expect(bin).toBe("/usr/local/bin/pi"); expect(args).toEqual(["--mode", "rpc"]); expect(options.cwd).toBe("/app/harness"); expect(options.env).toMatchObject({ PATH: "/usr/local/bin:/usr/bin", NODE_EXTRA_CA_CERTS: "/certs/company-ca.pem", THT_DATA_ROOT: "/data", THT_SESSION: "portable-session", THT_AUTHOR: "user@example.test", }); expect(options.env).not.toHaveProperty("PI_PROVIDER_API_KEY"); } finally { mgr.teardown("portable-session"); vi.unstubAllEnvs(); } }); test("session Pi spawn omits ambient THT_DATA_ROOT when config does not provide one", async () => { vi.stubEnv("THT_DATA_ROOT", "/ambient-must-not-leak"); vi.stubEnv("PI_PROVIDER_API_KEY", "still-inherited"); const calls: any[][] = []; const child = recordingChild(); child.stderr.resume = () => {}; const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi" }), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; }, }); try { await mgr.spawnFor("no-data-root", {}); expect(calls[0][2].env).not.toHaveProperty("THT_DATA_ROOT"); expect(calls[0][2].env).not.toHaveProperty("PI_PROVIDER_API_KEY"); } finally { mgr.teardown("no-data-root"); vi.unstubAllEnvs(); } }); test.each([ ["anthropic", "ANTHROPIC_API_KEY"], ["OpenAI", "OPENAI_API_KEY"], ["gemini", "GEMINI_API_KEY"], ["google", "GEMINI_API_KEY"], ["deepseek", "DEEPSEEK_API_KEY"], ["zai", "ZAI_API_KEY"], ["openrouter", "OPENROUTER_API_KEY"], ])("injects the generic file credential only as %s provider env", async (provider, expectedName) => { const secret = path.resolve(__dirname, `.model-key-${process.pid}-${provider}`); writeFileSync(secret, "provider-secret", { mode: 0o600 }); const calls: any[][] = []; const child = recordingChild(); child.stderr.resume = () => {}; const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi", THT_MODEL_API_KEY_FILE: secret, }), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; }, // Empty auth store: exercise the managed-key injection path deterministically, // independent of whatever providers the developer's ~/.pi/agent/auth.json holds. authProviders: () => new Set(), }); try { await mgr.spawnFor("credential-session", { provider }); const env = calls[0][2].env; expect(env[expectedName]).toBe("provider-secret"); expect(env).not.toHaveProperty("PI_PROVIDER_API_KEY"); expect(env).not.toHaveProperty("THT_MODEL_API_KEY_FILE"); expect(JSON.stringify(calls[0].slice(0, 2))).not.toContain("provider-secret"); } finally { mgr.teardown("credential-session"); await import("node:fs/promises").then((fs) => fs.unlink(secret)); } }); test("skips managed-key injection for a provider present in pi's auth store", async () => { const secret = path.resolve(__dirname, `.model-key-${process.pid}-authskip`); writeFileSync(secret, "provider-secret", { mode: 0o600 }); const calls: any[][] = []; const child = recordingChild(); child.stderr.resume = () => {}; const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi", THT_MODEL_API_KEY_FILE: secret, }), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; }, authProviders: () => new Set(["deepseek"]), }); try { await mgr.spawnFor("authskip-session", { provider: "deepseek" }); const env = calls[0][2].env; // pi resolves deepseek from its own auth store, so no key is forced onto it. expect(env.DEEPSEEK_API_KEY).toBeUndefined(); } finally { mgr.teardown("authskip-session"); await import("node:fs/promises").then((fs) => fs.unlink(secret)); } }); test("session Pi spawn reads the single secret bundle and scrubs its path", async () => { const secret = path.resolve(__dirname, `.bundle-${process.pid}`); writeFileSync(secret, [ "THT_MODEL_API_KEY=bundle-secret", "THT_DWH_API_KEY=dwh-secret", "THT_VEC_API_KEY=vector-reader-secret", "THT_VEC_WRITE_API_KEY=vector-writer-secret", "THT_CA=/run/secrets/ca-chain.pem", "", ].join("\n"), { mode: 0o600 }); chmodSync(secret, 0o600); const calls: any[][] = []; const child = recordingChild(); child.stderr.resume = () => {}; const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi", THT_SECRETS_FILE: secret, }), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; } }); try { await mgr.spawnFor("bundle-session", { provider: "openai" }); expect(calls[0][2].env.OPENAI_API_KEY).toBe("bundle-secret"); expect(calls[0][2].env).toMatchObject({ THT_DWH_API_KEY: "dwh-secret", THT_VEC_API_KEY: "vector-reader-secret", THT_VEC_WRITE_API_KEY: "vector-writer-secret", THT_CA: "/run/secrets/ca-chain.pem", THT_SSL_CA: "/run/secrets/ca-chain.pem", }); expect(calls[0][2].env).not.toHaveProperty("THT_SECRETS_FILE"); } finally { mgr.teardown("bundle-session"); await import("node:fs/promises").then((fs) => fs.unlink(secret)); } }); test.each([["OpenAI", "openai"], ["gemini", "google"]])( "set_model uses canonical packaged provider ID for %s", async (provider, canonical) => { const secret = path.resolve(__dirname, `.canonical-key-${process.pid}-${provider}`); writeFileSync(secret, "provider-secret", { mode: 0o600 }); const child = recordingChild(); child.stderr.resume = () => {}; child.stdin.write = (data: unknown) => { const request = JSON.parse(String(data)); child._writes.push(String(data)); if (request.id) { queueMicrotask(() => child.stdout.emit("data", `${JSON.stringify({ type: "response", id: request.id, success: true, })}\n`)); } return true; }; const mgr = new PiProcessManager(loadConfig({ THT_MODEL_API_KEY_FILE: secret }), { spawnFn: () => child as any, }); try { await mgr.spawnFor("canonical-provider", { provider, model: "model-id" }); expect(child._writes.join("")).toContain(`\"provider\":\"${canonical}\"`); } finally { mgr.teardown("canonical-provider"); await import("node:fs/promises").then((fs) => fs.unlink(secret)); } }, ); test.each(["ollama", "local-qwen"])( "local provider %s spawns without a model key and scrubs ambient credentials", async (provider) => { vi.stubEnv("PI_PROVIDER_API_KEY", "ambient-secret"); vi.stubEnv("THT_MODEL_API_KEY_FILE", "/ambient/secret-path"); vi.stubEnv("OPENAI_API_KEY", "unselected-provider-secret"); const calls: any[][] = []; const child = recordingChild(); child.stderr.resume = () => {}; const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi" }), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; }, }); try { await mgr.spawnFor(`local-session-${provider}`, { provider }); expect(calls[0][2].env).not.toHaveProperty("PI_PROVIDER_API_KEY"); expect(calls[0][2].env).not.toHaveProperty("THT_MODEL_API_KEY_FILE"); expect(calls[0][2].env).not.toHaveProperty("OPENAI_API_KEY"); } finally { mgr.teardown(`local-session-${provider}`); vi.unstubAllEnvs(); } }, ); test.each(["amazon-bedrock", "azure-openai-responses", "cloudflare-workers-ai", "cloudflare-ai-gateway"])( "session spawn rejects compound provider %s before spawning Pi", async (provider) => { const secret = path.resolve(__dirname, `.compound-key-${process.pid}-${provider}`); writeFileSync(secret, "provider-secret", { mode: 0o600 }); let spawns = 0; const mgr = new PiProcessManager(loadConfig({ THT_MODEL_API_KEY_FILE: secret }), { spawnFn: () => { spawns += 1; throw new Error("must not spawn"); }, }); try { await expect(mgr.spawnFor("compound-provider", { provider })).rejects.toThrow( "compound credential bundles are unsupported by THT_MODEL_API_KEY_FILE; dedicated provider configuration is required", ); expect(spawns).toBe(0); } finally { await import("node:fs/promises").then((fs) => fs.unlink(secret)); } }, ); test.each(["missing", "permissive", "unreadable", "directory", "symlink", "unsupported"])( "hosted provider credential failure is sanitized: %s", async (kind) => { const target = path.resolve(__dirname, `.bad-model-key-${process.pid}-${kind}`); if (kind === "permissive") { writeFileSync(target, "DO_NOT_LEAK", { mode: 0o644 }); chmodSync(target, 0o644); } else if (kind === "unreadable") { writeFileSync(target, "DO_NOT_LEAK", { mode: 0o000 }); } else if (kind === "directory") { await import("node:fs/promises").then((fs) => fs.mkdir(target)); } else if (kind === "symlink") { const source = `${target}-source`; writeFileSync(source, "DO_NOT_LEAK", { mode: 0o600 }); await import("node:fs/promises").then((fs) => fs.symlink(source, target)); } const cfg = loadConfig({ THT_MODEL_API_KEY_FILE: target }); const mgr = new PiProcessManager(cfg, { spawnFn: () => { throw new Error("spawn must not occur"); } }); try { const provider = kind === "unsupported" ? "unknown-hosted" : "anthropic"; await expect(mgr.spawnFor("bad-secret", { provider })) .rejects.toThrow("model provider credential is unavailable"); } finally { if (kind === "permissive") await import("node:fs/promises").then((fs) => fs.unlink(target)); if (kind === "unreadable") { chmodSync(target, 0o600); await import("node:fs/promises").then((fs) => fs.unlink(target)); } if (kind === "directory") await import("node:fs/promises").then((fs) => fs.rmdir(target)); if (kind === "symlink") { await import("node:fs/promises").then(async (fs) => { await fs.unlink(target); await fs.unlink(`${target}-source`); }); } } }, );