/** * Static guard tests for the error-sanitization fixes in release/v3.8.2. * * Review findings: two authenticated routes returned raw error text in their * HTTP body (`(error as Error).message` / `String(error)`), violating the * project's error-sanitization policy (CLAUDE.md hard rule 12, * docs/security/ERROR_SANITIZATION.md). These guards pin the fix in source so * the anti-pattern cannot silently return. (Static-source assertions mirror the * established style of cli-tools-auth-hardening.test.ts.) */ import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); function readRoute(rel: string): string { return fs.readFileSync(path.join(REPO_ROOT, rel), "utf8"); } const CACHE_STATS = "src/app/api/cache/stats/route.ts"; const HERMES = "src/app/api/cli-tools/hermes-agent-settings/route.ts"; const COPILOT_CHAT = "src/app/api/copilot/chat/route.ts"; const DB_IMPORT = "src/app/api/db-backups/import/route.ts"; test("db-backups/import awaits getSettings() before re-hydrating the system prompt", () => { // getSettings() is async; without `await`, importedSettings is a Promise and // `.systemPrompt` is always undefined, so the #2470 re-hydration silently // never fires after a DB import. const src = readRoute(DB_IMPORT); assert.match( src, /const importedSettings = await getSettings\(\);/, "must await getSettings() so systemPrompt re-hydration actually runs" ); assert.ok( !/const importedSettings = getSettings\(\);/.test(src), "must not read systemPrompt off an un-awaited Promise" ); }); test("cache/stats route routes errors through sanitizeErrorMessage", () => { const src = readRoute(CACHE_STATS); assert.match(src, /import \{ sanitizeErrorMessage \}/, "must import sanitizeErrorMessage"); assert.match(src, /sanitizeErrorMessage\(error\)/, "catch blocks must sanitize the error"); assert.ok( !/\(error as Error\)\.message/.test(src), "must not put raw (error as Error).message in the response body" ); }); test("hermes-agent-settings GET sanitizes its error response (no raw String(error))", () => { const src = readRoute(HERMES); assert.match(src, /import \{ sanitizeErrorMessage \}/, "must import sanitizeErrorMessage"); assert.match(src, /sanitizeErrorMessage\(error\)/, "GET catch must sanitize the error"); assert.ok( !/error: String\(error\)/.test(src), "must not return raw String(error) in the response body" ); }); test("hermes-agent-settings POST validates baseUrl as an http(s) URL", () => { const src = readRoute(HERMES); assert.match( src, /import \{ validateBaseUrl \}/, "must import the shared validateBaseUrl helper" ); assert.match(src, /validateBaseUrl\(baseUrl\)/, "POST must validate the supplied baseUrl"); }); test("hermes-agent-settings POST validates preview mode before use", () => { const src = readRoute(HERMES); assert.match(src, /preview: z\.boolean\(\)\.optional\(\)/, "preview must be schema-bound"); assert.match(src, /const \{ baseUrl, keyId, apiKey, selections, preview \} = parsed\.data;/); assert.match(src, /if \(preview === true\)/); assert.ok(!/body\.preview/.test(src), "must not reference an undefined body variable"); }); test("copilot chat route requires management auth and sanitizes thrown errors", () => { const src = readRoute(COPILOT_CHAT); assert.match(src, /requireManagementAuth/, "route should enforce management auth"); assert.match(src, /const authError = await requireManagementAuth\(request\);/); assert.match(src, /sanitizeErrorMessage\(error\)/, "catch blocks must sanitize errors"); assert.ok(!/error\.message/.test(src), "must not return raw error.message"); });