import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test, vi } from "vitest"; import { loadConfig } from "../src/config.js"; import { createPiManagement, type PiExecFile, } from "../src/pi/management.js"; import type { RuntimeModelCatalog } from "../src/models/runtime-model-catalog.js"; function configFor(settingsFile = join(mkdtempSync(join(tmpdir(), "tht-pi-management-")), "settings.json")) { return loadConfig({ THT_HARNESS_DIR: "../harness", SETTINGS_FILE: settingsFile, PI_BIN: "/usr/local/bin/pi", PI_MANAGEMENT_TIMEOUT_MS: "750", }); } const modelCatalog: RuntimeModelCatalog = { defaultSession: "zai/glm-5.2", defaultMetadataGeneration: null, embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 }, sessionModels: () => [], metadataModels: () => [], hasSession: (id) => id === "zai/glm-5.2", }; function successfulExec(calls: Array<{ command: string; args: string[]; timeout: number }>): PiExecFile { return async (command, args, options) => { calls.push({ command, args, timeout: options.timeout }); return { stdout: "pi 0.80.3\n", stderr: "" }; }; } // Catches a Pi executable that emits unexpected text or is invoked through a shell, which could // turn a version display into a command-injection or information-disclosure surface. test("status parses only a Pi version from a fixed execFile argument array", async () => { const calls: Array<{ command: string; args: string[]; timeout: number }> = []; const service = createPiManagement(configFor(), { execute: successfulExec(calls), modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), credentialStatus: () => "missing", now: () => new Date("2026-08-05T10:00:00.000Z"), }); await expect(service.status()).resolves.toEqual({ version: "0.80.3", ready: true, credentials: "missing", config: { provider: "zai", model: "glm-5.2", reasoning: "medium" }, checkedAt: "2026-08-05T10:00:00.000Z", }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ command: "/usr/local/bin/pi", args: ["--version"] }); expect(calls[0].timeout).toBeGreaterThan(0); expect(calls[0].timeout).toBeLessThanOrEqual(750); }); // Catches credential presence being inferred from smoke success/failure or exposing any // credential material instead of the installation's explicit sanitized presence state. test.each(["present", "missing"] as const)( "status reports configured-provider credentials only as %s", async (credentials) => { const checkedProviders: Array = []; const service = createPiManagement(configFor(), { execute: successfulExec([]), modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), credentialStatus: (provider) => { checkedProviders.push(provider); return credentials; }, now: () => new Date("2026-08-05T10:00:00.000Z"), }); const status = await service.status(); expect(status).toEqual({ version: "0.80.3", ready: true, credentials, config: { provider: "zai", model: "glm-5.2", reasoning: "medium" }, checkedAt: "2026-08-05T10:00:00.000Z", }); expect(checkedProviders).toEqual(["zai"]); expect(JSON.stringify(status)).not.toMatch(/api.?key|token|password|secret/i); }, ); // Catches a hung Pi smoke check that leaves an operator waiting indefinitely or returns raw child // diagnostics containing provider credentials. test("smoke uses the configured timeout and reports a sanitized timeout", async () => { const calls: Array<{ command: string; args: string[]; timeout: number }> = []; const service = createPiManagement(configFor(), { execute: async (command, args, options) => { calls.push({ command, args, timeout: options.timeout }); throw Object.assign(new Error("provider token=raw-provider-token"), { code: "ETIMEDOUT" }); }, modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), now: () => new Date("2026-08-05T10:00:00.000Z"), }); await expect(service.test()).resolves.toEqual({ ready: false, message: "Pi smoke check timed out", checkedAt: "2026-08-05T10:00:00.000Z", }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ command: "/usr/local/bin/pi", args: ["--version"] }); expect(calls[0]!.timeout).toBeGreaterThan(0); expect(calls[0]!.timeout).toBeLessThanOrEqual(750); }); // Catches a smoke endpoint that validates only the Pi binary/model catalogue and never makes a // request through the configured provider and model. test("smoke exercises the configured provider and model", async () => { const providerChecks: unknown[] = []; const service = createPiManagement(configFor(), { execute: successfulExec([]), modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), smokeProvider: async (request) => { providerChecks.push(request); }, now: () => new Date("2026-08-05T10:00:00.000Z"), }); await expect(service.test()).resolves.toEqual({ ready: true, checkedAt: "2026-08-05T10:00:00.000Z", }); expect(providerChecks).toEqual([{ provider: "zai", model: "glm-5.2", reasoning: "medium", timeoutMs: expect.any(Number), }]); }); // Catches expired provider credentials being treated as ready or raw provider diagnostics being // reflected through the management API. test("smoke fails closed and sanitizes configured-provider authentication errors", async () => { const service = createPiManagement(configFor(), { execute: successfulExec([]), modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), smokeProvider: async () => { throw new Error('401 {"token":"raw-expired-token","output":"raw-provider-output"}'); }, now: () => new Date("2026-08-05T10:00:00.000Z"), }); const result = await service.test(); expect(result).toEqual({ ready: false, message: "Pi provider smoke check failed", checkedAt: "2026-08-05T10:00:00.000Z", }); expect(JSON.stringify(result)).not.toMatch(/raw-expired-token|raw-provider-output/); }); // Catches selected auth/models validation failures being downgraded to a generic provider error // or exposing the rejected command, path, or secret through POST /pi-management/test. test("smoke reports invalid managed provider configuration with a stable sanitized error", async () => { const service = createPiManagement(configFor(), { execute: successfulExec([]), modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), smokeProvider: async () => { throw Object.assign( new Error("!sensitive-command /private/models.json raw-secret"), { code: "PI_MANAGED_CONFIG_INVALID" }, ); }, now: () => new Date("2026-08-05T10:00:00.000Z"), }); const result = await service.test(); expect(result).toEqual({ ready: false, message: "Pi provider/model configuration is invalid", checkedAt: "2026-08-05T10:00:00.000Z", }); expect(JSON.stringify(result)).not.toMatch(/sensitive|private|models\.json|secret/i); }); // Catches separate per-phase timeouts that allow a later provider turn to exceed the one // end-to-end Pi Management smoke budget. test("smoke applies one deadline across version and a hung provider turn", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-08-05T10:00:00.000Z")); try { const providerTimeouts: number[] = []; const service = createPiManagement(configFor(), { execute: async () => await new Promise((resolve) => setTimeout( () => resolve({ stdout: "pi 0.80.3\n", stderr: "" }), 500, )), modelCatalog, readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }), smokeProvider: async ({ timeoutMs }) => { providerTimeouts.push(timeoutMs); await new Promise(() => {}); }, now: () => new Date("2026-08-05T10:00:00.000Z"), }); let settled = false; const pending = service.test().finally(() => { settled = true; }); await vi.advanceTimersByTimeAsync(500); expect(providerTimeouts).toEqual([250]); await vi.advanceTimersByTimeAsync(249); expect(settled).toBe(false); await vi.advanceTimersByTimeAsync(1); await expect(pending).resolves.toEqual({ ready: false, message: "Pi smoke check timed out", checkedAt: "2026-08-05T10:00:00.000Z", }); } finally { vi.useRealTimers(); } }); // Catches an unbounded diagnostics endpoint or one that returns bearer tokens and connection // passwords captured in Pi output. test("logs keep only the latest 200 redacted lines", async () => { const source = Array.from({ length: 205 }, (_, index) => `line-${index + 1}`); source[203] = "Authorization: Bearer raw-bearer-token"; source[204] = "database_url=postgres://thoth:raw-db-password@example.invalid/db"; source[202] = '{"token":"raw-json-secret","password":"raw-json-password"}'; source[201] = "THT_MODEL_API_KEY=raw-env-secret"; const service = createPiManagement(configFor(), { execute: successfulExec([]), modelCatalog, readLogs: () => source.join("\n"), now: () => new Date("2026-08-05T10:00:00.000Z"), }); const logs = await service.logs(); expect(logs.checkedAt).toBe("2026-08-05T10:00:00.000Z"); expect(logs.lines).toHaveLength(200); expect(logs.lines[0]).toBe("line-6"); expect(logs.lines.join("\n")).not.toContain("raw-bearer-token"); expect(logs.lines.join("\n")).not.toContain("raw-db-password"); expect(logs.lines.join("\n")).not.toContain("raw-json-secret"); expect(logs.lines.join("\n")).not.toContain("raw-json-password"); expect(logs.lines.join("\n")).not.toContain("raw-env-secret"); expect(logs.lines.join("\n")).toContain("[REDACTED]"); });