Files
ThothII/backend/test/pi-process-manager.test.ts
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

903 lines
37 KiB
TypeScript

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 type { RuntimeModelCatalog, RuntimeModel } from "../src/models/runtime-model-catalog.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","apiKey":"local","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<any>((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<void>((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("Pi language launch hint comes from the session options and never ambient environment", () => {
const previous = process.env.THT_INTERACTION_LANGUAGE;
process.env.THT_INTERACTION_LANGUAGE = "it";
const environments: NodeJS.ProcessEnv[] = [];
const mgr = new PiProcessManager(loadConfig({}), {
spawnFn: (_command, _args, options) => {
environments.push(options.env);
return recordingChild() as any;
},
});
try {
mgr.createFor("explicit-language", { interactionLanguage: "en" });
mgr.teardown("explicit-language");
mgr.createFor("manifest-resolved-in-gate");
mgr.teardown("manifest-resolved-in-gate");
expect(environments[0].THT_INTERACTION_LANGUAGE).toBe("en");
expect(environments[1]).not.toHaveProperty("THT_INTERACTION_LANGUAGE");
} finally {
if (previous === undefined) delete process.env.THT_INTERACTION_LANGUAGE;
else process.env.THT_INTERACTION_LANGUAGE = previous;
}
});
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 <id>", 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([false, true])("session Pi uses the catalog bundle despite stale Pi auth: %s", (missingKey) => {
const root = mkdtempSync(path.join(tmpdir(), "thothii-catalog-credential-"));
const agentDir = path.join(root, "agent");
mkdirSync(agentDir, { mode: 0o700 });
const originalAuth = JSON.stringify({
deepseek: { type: "api_key", key: "stale-key" },
anthropic: { type: "api_key", key: "unrelated-key" },
});
writeFileSync(path.join(agentDir, "auth.json"), originalAuth, { mode: 0o600 });
writeFileSync(path.join(agentDir, "models.json"), '{"providers":{}}\n', { mode: 0o600 });
const secret = path.join(root, "thothii.secrets");
writeFileSync(secret, missingKey ? "THT_MODEL_API_KEY=legacy-secret\n"
: "DEEPSEEK_API_KEY=catalog-secret\nTHT_MODEL_API_KEY=legacy-secret\n", { mode: 0o600 });
const legacyKey = path.join(root, "legacy-key");
writeFileSync(legacyKey, "legacy-file-key", { mode: 0o600 });
const model: RuntimeModel = {
id: "deepseek/deepseek-v4-pro",
provider: "deepseek",
model: "deepseek-v4-pro",
label: "DeepSeek V4 Pro",
upstreamModel: "deepseek-v4-pro",
authentication: { mode: "secret_env", apiKeyEnv: "DEEPSEEK_API_KEY" },
sessionAdapter: { mode: "pi_builtin" },
session: { reasoning: false },
};
const modelCatalog: RuntimeModelCatalog = {
defaultInteraction: model.id,
embedding: null,
sessionModels: () => [model],
metadataModels: () => [],
hasSession: (id) => id === model.id,
};
const calls: any[][] = [];
const child = recordingChild();
child.stderr.resume = () => {};
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
const mgr = new PiProcessManager(loadConfig({ THT_SECRETS_FILE: secret, THT_MODEL_API_KEY_FILE: legacyKey }), {
modelCatalog,
authProviders: () => new Set(["deepseek"]),
spawnFn: (...args: any[]) => { calls.push(args); return child as any; },
});
try {
const create = () => mgr.createFor("catalog-credential", { provider: model.provider, model: model.model });
if (missingKey) {
expect(create).toThrow("model provider credential is unavailable");
expect(calls).toHaveLength(0);
return;
}
create();
expect(calls[0][2].env.DEEPSEEK_API_KEY).toBe("catalog-secret");
expect(calls[0][2].env).not.toHaveProperty("OPENAI_API_KEY");
expect(calls[0][2].env).not.toHaveProperty("THT_MODEL_API_KEY");
expect(JSON.parse(readFileSync(path.join(calls[0][2].env.PI_CODING_AGENT_DIR, "auth.json"), "utf8")))
.toEqual({ anthropic: { type: "api_key", key: "unrelated-key" } });
} finally {
expect(readFileSync(path.join(agentDir, "auth.json"), "utf8")).toBe(originalAuth);
mgr.teardown("catalog-credential");
vi.unstubAllEnvs();
rmSync(root, { recursive: true, force: true });
}
});
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("set_model translates a canonical catalog key to its upstream Pi model ID", async () => {
const root = mkdtempSync(path.join(tmpdir(), "thothii-upstream-model-"));
const agentDir = path.join(root, "agent");
mkdirSync(agentDir, { mode: 0o700 });
writeFileSync(path.join(agentDir, "models.json"), JSON.stringify({
providers: {
local: {
baseUrl: "http://ollama:11434/v1", apiKey: "local",
models: [{ id: "qwen2.5:7b" }],
},
},
}), { mode: 0o600 });
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
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 model: RuntimeModel = {
id: "local/qwen", provider: "local", model: "qwen", label: "Qwen",
upstreamModel: "qwen2.5:7b", endpoint: { baseUrl: "http://ollama:11434/v1" },
authentication: { mode: "none" }, sessionAdapter: { mode: "openai_compatible" },
session: { reasoning: false, contextWindow: 32768, maxTokens: 8192 },
};
const modelCatalog: RuntimeModelCatalog = {
defaultInteraction: model.id, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
};
const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi" }), {
modelCatalog, authProviders: () => new Set(), spawnFn: () => child as any,
});
try {
await mgr.spawnFor("upstream-model", { provider: "local", model: "qwen" });
expect(child._writes.join("")).toContain('"modelId":"qwen2.5:7b"');
} finally {
mgr.teardown("upstream-model");
vi.unstubAllEnvs();
rmSync(root, { recursive: true, force: true });
}
});
test.each(["installation-local", "private-compatible"])(
"provider %s configured with a literal apiKey spawns without a managed key",
async (provider) => {
const root = mkdtempSync(path.join(tmpdir(), "tht-local-provider-"));
const agentDir = path.join(root, "agent");
mkdirSync(agentDir, { mode: 0o700 });
writeFileSync(path.join(agentDir, "models.json"), JSON.stringify({
providers: {
[provider]: {
baseUrl: "http://model.invalid/v1",
apiKey: "local",
models: [{ id: "model" }],
},
},
}), { mode: 0o600 });
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
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();
rmSync(root, { recursive: true, force: true });
}
},
);
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`);
});
}
}
},
);