From dc66426462beeafd022bd886df72ef24b56b4766 Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:11:09 -0300 Subject: [PATCH] test: isolate stateful error boundary fixtures --- tests/unit/calllogs-format-split.test.ts | 28 +- .../unit/chatcore-stream-error-result.test.ts | 29 +- tests/unit/combo-diagnostics-trace.test.ts | 21 - .../error-public-boundaries-hardening.test.ts | 609 +----------------- tests/unit/error-sensitive-redaction.test.ts | 28 +- ...ror-public-boundaries-hardening.fixture.ts | 608 +++++++++++++++++ .../mcp-public-error-boundaries.fixture.ts | 184 ++++++ ...onnection-test-error-boundaries.fixture.ts | 262 ++++++++ ...rovider-last-error-sanitization.fixture.ts | 109 ++++ ...request-log-management-boundary.fixture.ts | 108 ++++ ...ilure-persistent-classification.fixture.ts | 91 +++ .../gemini-responses-error-redaction.test.ts | 26 +- .../helpers/runIsolatedBoundaryFixture.ts | 73 +++ .../unit/mcp-public-error-boundaries.test.ts | 187 +----- tests/unit/moderations-handler.test.ts | 21 - tests/unit/ocr-handler-dispatch.test.ts | 24 +- ...r-connection-test-error-boundaries.test.ts | 266 +------- .../provider-last-error-sanitization.test.ts | 110 +--- ...ider-validation-error-sanitization.test.ts | 30 +- .../request-log-management-boundary.test.ts | 109 +--- tests/unit/request-log-payloads.test.ts | 24 +- ...-failure-persistent-classification.test.ts | 95 +-- ...stream-passthrough-error-redaction.test.ts | 26 +- tests/unit/upstream-error-passthrough.test.ts | 30 +- 24 files changed, 1505 insertions(+), 1593 deletions(-) create mode 100644 tests/unit/fixtures/error-public-boundaries-hardening.fixture.ts create mode 100644 tests/unit/fixtures/mcp-public-error-boundaries.fixture.ts create mode 100644 tests/unit/fixtures/provider-connection-test-error-boundaries.fixture.ts create mode 100644 tests/unit/fixtures/provider-last-error-sanitization.fixture.ts create mode 100644 tests/unit/fixtures/request-log-management-boundary.fixture.ts create mode 100644 tests/unit/fixtures/stream-failure-persistent-classification.fixture.ts create mode 100644 tests/unit/helpers/runIsolatedBoundaryFixture.ts diff --git a/tests/unit/calllogs-format-split.test.ts b/tests/unit/calllogs-format-split.test.ts index 5f86794614..4bb267bfab 100644 --- a/tests/unit/calllogs-format-split.test.ts +++ b/tests/unit/calllogs-format-split.test.ts @@ -15,22 +15,9 @@ * pass-through here — the test pins the formatting logic, not PII behaviour. */ -import { after, describe, it } from "node:test"; +import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-calllogs-format-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { +import { asRecord, toNumber, toStringOrNull, @@ -39,16 +26,7 @@ const { normalizeDetailState, toStoredErrorSummary, buildRequestSummary, -} = await import("../../src/lib/usage/callLogs/format.ts"); - -after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); +} from "../../src/lib/usage/callLogs/format.ts"; describe("callLogs/format — coercers", () => { it("asRecord keeps plain objects, rejects arrays/primitives/null", () => { diff --git a/tests/unit/chatcore-stream-error-result.test.ts b/tests/unit/chatcore-stream-error-result.test.ts index bdfe296e5d..3ebe5821e7 100644 --- a/tests/unit/chatcore-stream-error-result.test.ts +++ b/tests/unit/chatcore-stream-error-result.test.ts @@ -6,30 +6,11 @@ // the string-code extraction. import { test } from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-error-result-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { isSemaphoreCapacityError, createStreamingErrorResult, getUpstreamErrorIdentifier } = - await import("../../open-sse/handlers/chatCore/streamErrorResult.ts"); - -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); +import { + isSemaphoreCapacityError, + createStreamingErrorResult, + getUpstreamErrorIdentifier, +} from "../../open-sse/handlers/chatCore/streamErrorResult.ts"; test("isSemaphoreCapacityError matches the two semaphore codes only", () => { assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_TIMEOUT" }), true); diff --git a/tests/unit/combo-diagnostics-trace.test.ts b/tests/unit/combo-diagnostics-trace.test.ts index 590dbddfe6..dc1354f7ca 100644 --- a/tests/unit/combo-diagnostics-trace.test.ts +++ b/tests/unit/combo-diagnostics-trace.test.ts @@ -8,32 +8,11 @@ */ import test from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-diagnostics-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); const { errorResponseWithComboDiagnostics, sanitizeComboDiagnostics } = await import("../../open-sse/utils/error.ts"); const { buildRecoveryHint } = await import("../../open-sse/services/combo/pinRecovery.ts"); -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - test("combo diagnostics: headers + body carry the sanitized trace (code override preserved)", async () => { const res = errorResponseWithComboDiagnostics( 503, diff --git a/tests/unit/error-public-boundaries-hardening.test.ts b/tests/unit/error-public-boundaries-hardening.test.ts index a337ebd833..8e0e202b5b 100644 --- a/tests/unit/error-public-boundaries-hardening.test.ts +++ b/tests/unit/error-public-boundaries-hardening.test.ts @@ -1,608 +1,11 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; -import { fileURLToPath } from "node:url"; -const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-public-errors-")); -const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); -const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); -const ORIGINAL_DATA_DIR = process.env.DATA_DIR; -const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; -const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url)); +import { runIsolatedBoundaryFixture } from "./helpers/runIsolatedBoundaryFixture.ts"; -fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); -fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); -process.env.DATA_DIR = TEST_DATA_DIR; -process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; - -const core = await import("../../src/lib/db/core.ts"); -const { - buildErrorBody, - buildModelCooldownBody, - createErrorResult, - parseUpstreamError, - projectPublicErrorIdentifier, - providerCircuitOpenResponse, - sanitizeErrorMessage, - sanitizeUpstreamDetails, - unavailableResponse, -} = await import("../../open-sse/utils/error.ts"); -const { buildPassthroughErrorResponse, shouldPassthroughUpstreamError } = - await import("../../open-sse/utils/upstreamErrorPassthrough.ts"); - -test.after(() => { - core.resetDbInstance(); - if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = ORIGINAL_DATA_DIR; - if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; - fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - -test("sanitizeErrorMessage removes non-source paths, credentials, and serialized stacks", () => { - const raw = String.raw`Provider failed at /srv/private/provider-key.json access_token=provider-secret\n at validate (C:\Users\admin\private\validator.ts:42:7)`; - const safe = sanitizeErrorMessage(raw); - - assert.match(safe, /Provider failed/i); - assert.doesNotMatch(safe, /srv\/private|provider-secret|C:\\Users|validator\.ts/i); - assert.doesNotMatch(safe, /\\n\s*at validate/i); -}); - -test("sanitizeErrorMessage redacts Windows drive-root-relative filesystem paths", () => { - const plain = sanitizeErrorMessage( - String.raw`Provider failed at \Users\admin\private\secret.txt` - ); - const quoted = sanitizeErrorMessage( - String.raw`Provider failed opening "\Windows\Temp\native.dll"` - ); - const singleSegment = sanitizeErrorMessage(String.raw`Provider failed opening \private.db`); - const prose = sanitizeErrorMessage(String.raw`Provider reported \offline without a path`); - const escapedInitialPaths = [ - String.raw`Provider failed at \bin\private.db`, - String.raw`Provider failed at \folder\private.db`, - String.raw`Provider failed at \new\private.db`, - String.raw`Provider failed at \root\private.db`, - String.raw`Provider failed at \temp\private.db`, - String.raw`Provider failed at C:\temp\private.db`, - ].map((message) => sanitizeErrorMessage(message)); - - assert.equal(plain, "Provider failed at "); - assert.equal(quoted, 'Provider failed opening ""'); - assert.equal(singleSegment, "Provider failed opening "); - assert.equal(prose, String.raw`Provider reported \offline without a path`); - for (const projected of escapedInitialPaths) { - assert.equal(projected, "Provider failed at "); - } -}); - -test("sanitizeErrorMessage redacts extensionless POSIX paths without hiding explicit routes", () => { - const compact = sanitizeErrorMessage("Provider failed at /custom/internal/secret"); - const spaced = sanitizeErrorMessage("Provider failed at /custom/internal secret directory"); - const route = sanitizeErrorMessage("Route /dashboard/providers is unavailable"); - const singleSegment = sanitizeErrorMessage("Provider failed opening /vault"); - const singleSegmentRoute = sanitizeErrorMessage("Route /vault is unavailable"); - const compoundPathAndRoute = sanitizeErrorMessage( - "Failed /vault then GET /home/profile returned 404" - ); - const knownRootRoutes = [ - sanitizeErrorMessage("GET /home returned 404"), - sanitizeErrorMessage("Route /run is unavailable"), - sanitizeErrorMessage("POST /data returned 409"), - sanitizeErrorMessage("Route /var is unavailable"), - ]; - const body = buildErrorBody(500, "Provider failed at /custom/internal/secret"); - - assert.doesNotMatch(compact, /custom\/internal\/secret/); - assert.doesNotMatch(spaced, /custom\/internal|secret directory/); - assert.doesNotMatch(body.error.message, /custom\/internal\/secret/); - assert.match(compact, //); - assert.equal(route, "Route /dashboard/providers is unavailable"); - assert.equal(singleSegment, "Provider failed opening "); - assert.equal(singleSegmentRoute, "Route /vault is unavailable"); - assert.equal(compoundPathAndRoute, "Failed then GET /home/profile returned 404"); - assert.deepEqual(knownRootRoutes, [ - "GET /home returned 404", - "Route /run is unavailable", - "POST /data returned 409", - "Route /var is unavailable", - ]); -}); - -test("sanitizeErrorMessage fails closed when string coercion is hostile", () => { - const hostile = { - toString(): never { - throw new Error("access_token=hostile-secret at /srv/private/hostile.ts:1:2"); - }, - }; - - assert.equal(sanitizeErrorMessage(hostile), ""); -}); - -test("buildErrorBody projects untrusted error classifications onto safe identifiers", () => { - const body = buildErrorBody(502, "upstream failed", undefined, { - type: "server_error\nX-Leak: yes", - code: "sk-live-secret-value", - reason: "access_token=reason-secret", +test("public error boundaries pass in an isolated child process", () => { + runIsolatedBoundaryFixture({ + fixtureUrl: new URL("./fixtures/error-public-boundaries-hardening.fixture.ts", import.meta.url), + expectedTests: 23, + label: "public error boundaries", }); - - assert.equal(body.error.type, "server_error"); - assert.equal(body.error.code, "bad_gateway"); - assert.equal(body.error.reason, undefined); -}); - -test("createErrorResult rejects opaque upstream identifiers that could be echoed credentials", async () => { - const opaqueCredential = "AbC9xY7pQ2mN8vR4kL6z"; - const result = createErrorResult( - 502, - "upstream failed", - null, - opaqueCredential, - opaqueCredential - ); - const body = (await result.response.json()) as { - error: { code: string; type: string }; - }; - - assert.equal(body.error.code, "bad_gateway"); - assert.equal(body.error.type, "server_error"); - assert.doesNotMatch(JSON.stringify(body), new RegExp(opaqueCredential)); -}); - -test("parseUpstreamError never stringifies an untrusted error object into the public message", async () => { - const opaqueIdentifier = "AbC9xY7pQ2mN8vR4kL6z"; - const parsed = await parseUpstreamError( - Response.json( - { - error: { - code: opaqueIdentifier, - type: opaqueIdentifier, - reason: opaqueIdentifier, - }, - }, - { status: 502 } - ), - "openai" - ); - const result = createErrorResult( - parsed.statusCode, - parsed.message, - parsed.retryAfterMs, - parsed.errorCode as string, - parsed.errorType as string, - parsed.responseBody - ); - const bodyText = await result.response.text(); - - assert.equal(parsed.message, "Upstream error: 502"); - assert.doesNotMatch(bodyText, new RegExp(opaqueIdentifier)); -}); - -test("buildErrorBody preserves the configured empty code for unmapped client statuses", () => { - const body = buildErrorBody(424, "Dependency failed"); - - assert.equal(body.error.type, "invalid_request_error"); - assert.equal(body.error.code, ""); -}); - -test("public identifier vocabulary preserves current internal machine-readable contracts", () => { - const identifiers = [ - "context_length_exceeded", - "tool_calling_not_supported", - "vision", - "tools", - "structured_output", - "context_window", - "unsupported_endpoint", - "unverified_codex_client", - "invalid_previous_response_binding", - "incompatible_reasoning_effort", - "STREAM_READINESS_TIMEOUT", - "stream_timeout", - "STREAM_EARLY_EOF", - "stream_early_eof", - "LEASE_NO_ELIGIBLE_CONNECTION", - "LEASE_ELIGIBILITY_UNAVAILABLE", - "LEASE_UNSUPPORTED_ROUTE", - "LEASE_UNSUPPORTED_TRANSPORT", - "DIRECT_RESPONSE_START_TIMEOUT", - "PROXY_FAMILY_UNAVAILABLE", - "RELAY_TIMEOUT", - "TLS_FINGERPRINT_FAILED", - "PROXY_REQUEST_FAILED", - "TLS_SESSION_CAPACITY", - "TLS_CIRCUIT_OPEN", - "PROVIDER_RETIRED", - "upstream_empty_response", - "upstream_response_error", - "upstream_response_failed", - "stream_pipeline_error", - "stream_terminated", - "rate_limited", - "usage_limit_reached", - "timeout", - "semaphore_timeout", - "semaphore_queue_full", - "RATE_LIMIT_EXECUTION_TIMEOUT", - "RATE_LIMIT_QUEUE_FULL", - "RATE_LIMIT_QUEUE_WEDGED", - "RATE_LIMIT_QUEUE_TIMEOUT", - "rate_limit_queue_wedged", - "429", - "empty_response", - "stream_idle_timeout", - "empty_content", - "UNAVAILABLE", - "RESOURCE_EXHAUSTED", - "provider_unavailable", - "unsupported_feature", - "missing_project_id", - "oauth_missing_project_id", - "gcp_project_required", - "QUOTA_ONLY", - "QUOTA_NOT_ALLOCATED", - "cloudflare_challenge", - "cf_mitigated_challenge", - "upstream_protocol_error", - "claude_web_protocol_error", - "service_not_running", - "storage_encryption_stale", - "HTTP_429", - "BLACKBOX_SUBSCRIPTION_REQUIRED", - "BLACKBOX_AUTH_REQUIRED", - "BLACKBOX_RATE_LIMIT", - "abort", - "ABORTED", - "CHIPOTLE_ERROR", - "premium_model_requires_key", - "GROK_ERROR", - "TLS_CLIENT_UNAVAILABLE", - "upstream_access_denied", - "proxy_unavailable", - "EXECUTOR_ERROR", - "executor_contract_violation", - "orphan_tool_result", - "bedrock_stream_error", - "invalid_kiro_tool_call", - "devin_cli_error", - "upstream_websocket_error", - "upstream_websocket_connect_failed", - "codex_app_server_turn_failed", - "missing_credits", - "reached_limit", - "rate_limit_reached", - "rate_limit_longer_reached", - "client_cancelled", - "client_closed_request", - "compaction_control_unavailable", - "compaction_handoff_failed", - "connector_not_found", - "connector_error", - "prompt_attachment_integrity", - "chatgpt_session_expired", - "chatgpt_subscription_unavailable", - "upstream_server_error", - "multipart_protocol_violation", - "browser_stream_inconsistent", - "structured_output_validation_failed", - "chatgpt_submission_ambiguous", - "chatgpt_submitted_turn_failed", - "cli_not_found", - "upstream_auth_error", - "wreq_unavailable", - "api_error", - "connection_error", - "unsupported_runtime", - "VIDEO_ARTIFACT_URL_INVALID", - "VIDEO_ARTIFACT_URL_BLOCKED", - "VIDEO_ARTIFACT_DOWNLOAD_FAILED", - "VIDEO_ARTIFACT_TOO_LARGE", - "VIDEO_ARTIFACT_SIGNATURE_INVALID", - "VIDEO_ARTIFACT_NOT_READY", - "VIDEO_ARTIFACT_UNAVAILABLE", - "VIDEO_ARTIFACT_CONTENT_TYPE_INVALID", - "codex_app_server_unconfigured", - "meta_ai_warmup_failed", - "meta_ai_mode_switch_failed", - "meta_ai_ws_error", - "meta_ai_empty_response", - "PPLX_ERROR", - "cloudflare_or_bot", - "request_failed", - "lmarena_error", - "network_error", - ]; - - for (const identifier of identifiers) { - assert.equal(projectPublicErrorIdentifier(identifier, "bad_request"), identifier, identifier); - } -}); - -test("public numeric identifiers are limited to three-digit HTTP status codes", () => { - assert.equal(projectPublicErrorIdentifier("100", "bad_request"), "100"); - assert.equal(projectPublicErrorIdentifier("599", "bad_request"), "599"); - assert.equal(projectPublicErrorIdentifier("099", "bad_request"), "bad_request"); - assert.equal(projectPublicErrorIdentifier("600", "bad_request"), "bad_request"); - assert.equal(projectPublicErrorIdentifier("5000", "bad_request"), "bad_request"); - assert.equal(projectPublicErrorIdentifier("40002", "bad_request"), "bad_request"); - assert.equal(projectPublicErrorIdentifier("HTTP_600", "bad_request"), "bad_request"); - assert.equal(projectPublicErrorIdentifier("HTTP_40002", "bad_request"), "bad_request"); - assert.equal(projectPublicErrorIdentifier("weird_error", "bad_gateway"), "bad_gateway"); -}); - -test("buildErrorBody callers never overwrite a projected public classification", () => { - const productionFiles: string[] = []; - const collectTypeScriptFiles = (directory: string): void => { - for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - if (entry.name === "__tests__") continue; - collectTypeScriptFiles(entryPath); - } else if (entry.isFile() && /\.tsx?$/.test(entry.name)) { - productionFiles.push(entryPath); - } - } - }; - - collectTypeScriptFiles(path.join(REPO_ROOT, "open-sse")); - collectTypeScriptFiles(path.join(REPO_ROOT, "src")); - - const mutationPattern = /\b[A-Za-z_$][A-Za-z0-9_$]*\.error\.(?:code|type|reason)\s*=(?!=)/g; - const violations: string[] = []; - for (const filePath of productionFiles) { - const source = fs.readFileSync(filePath, "utf8"); - if (!source.includes("buildErrorBody")) continue; - for (const match of source.matchAll(mutationPattern)) { - const line = source.slice(0, match.index).split("\n").length; - violations.push(`${path.relative(REPO_ROOT, filePath)}:${line}`); - } - } - - assert.deepEqual(violations, []); - - const chatCoreSource = fs.readFileSync( - path.join(REPO_ROOT, "open-sse/handlers/chatCore.ts"), - "utf8" - ); - assert.doesNotMatch( - chatCoreSource, - /JSON\.stringify\(\s*\{\s*error\s*:\s*\{/, - "chatCore must not bypass buildErrorBody with a manually assembled error envelope" - ); -}); - -test("operational log persistence catches use the canonical sanitizer", () => { - const callLogsSource = fs.readFileSync(path.join(REPO_ROOT, "src/lib/usage/callLogs.ts"), "utf8"); - const proxyLoggerSource = fs.readFileSync(path.join(REPO_ROOT, "src/lib/proxyLogger.ts"), "utf8"); - - assert.match(callLogsSource, /sanitizeErrorMessage\(error\)/); - assert.doesNotMatch(callLogsSource, /\(error as Error\)\.message/); - assert.match(proxyLoggerSource, /sanitizeErrorMessage\(err\)/); - assert.doesNotMatch(proxyLoggerSource, /err\?\.message\s*\|\|\s*err/); -}); - -test("stream request finalization never warns with a raw error object", () => { - const source = fs.readFileSync( - path.join(REPO_ROOT, "open-sse/utils/streamFailureFinalization.ts"), - "utf8" - ); - - assert.match(source, /sanitizeErrorMessage\(error\)/); - assert.doesNotMatch(source, /"message" in error[\s\S]{0,160}: error/); -}); - -test("chatCore provider-failure writes use the projected persistent message", () => { - const source = fs.readFileSync(path.join(REPO_ROOT, "open-sse/handlers/chatCore.ts"), "utf8"); - const failureStart = source.indexOf("providerFailure: if (!providerResponse.ok)"); - const failureEnd = source.indexOf("// Non-streaming response", failureStart); - assert.ok(failureStart >= 0 && failureEnd > failureStart, "providerFailure block must exist"); - const failureBlock = source.slice(failureStart, failureEnd); - - assert.doesNotMatch(failureBlock, /lastError:\s*message\b/); - assert.ok( - (failureBlock.match(/lastError:\s*persistentMessage\b/g) || []).length >= 11, - "every providerFailure persistence branch must use persistentMessage" - ); -}); - -test("public cooldown and circuit responses sanitize dynamic context", async () => { - const unavailable = unavailableResponse( - 503, - "Provider failed at /srv/private/state.sqlite access_token=unavailable-secret", - 5, - "retry after reading C:\\Users\\admin\\private\\state.json" - ); - const unavailableBody = (await unavailable.json()) as { error: { message: string } }; - assert.doesNotMatch(unavailableBody.error.message, /srv\/private|unavailable-secret|C:\\Users/i); - - const circuit = providerCircuitOpenResponse( - "provider access_token=circuit-secret /home/service/provider.json", - 5 - ); - const circuitBody = (await circuit.json()) as { - error: { message: string; provider: string }; - }; - assert.equal(circuitBody.error.provider, "unknown"); - assert.doesNotMatch(JSON.stringify(circuitBody), /circuit-secret|\/home\/service/i); - - const cooldown = buildModelCooldownBody({ - model: "model access_token=model-secret /opt/models/private.json", - retryAfterSec: Number.NaN, - retryAfterAt: "not-a-timestamp access_token=timestamp-secret", - }); - assert.equal(cooldown.error.model, undefined); - assert.equal(cooldown.error.retry_after, undefined); - assert.equal(cooldown.error.reset_seconds, 1); - assert.doesNotMatch(JSON.stringify(cooldown), /model-secret|timestamp-secret|\/opt\/models/i); -}); - -test("sanitizeUpstreamDetails drops credential aliases and prototype-control keys", () => { - const input = Object.create(null) as Record; - input.error = { - message: "quota metadata at /srv/provider/private.json", - credential: "credential-secret", - sessionId: "session-secret", - session_count: 2, - }; - input.__proto__ = { leaked: true }; - - const safe = sanitizeUpstreamDetails(input) as Record; - const serialized = JSON.stringify(safe); - - assert.doesNotMatch(serialized, /credential-secret|session-secret|srv\/provider|__proto__/i); - assert.match(serialized, /"session_count":2/); -}); - -test("buildErrorBody fails closed for hostile upstream detail accessors", () => { - const hostile = new Proxy( - {}, - { - ownKeys(): never { - throw new Error("access_token=hostile-detail at /srv/private/detail.ts:1:2"); - }, - } - ); - - let body: ReturnType | undefined; - assert.doesNotThrow(() => { - body = buildErrorBody(502, "upstream failed", hostile); - }); - assert.equal(body?.upstream_details, undefined); - assert.doesNotMatch(JSON.stringify(body), /hostile-detail|srv\/private|detail\.ts/i); -}); - -test("upstream passthrough preserves safe wording but recursively sanitizes the JSON body", async () => { - const opaqueIdentifier = "AbC9xY7pQ2mN8vR4kL6z"; - const upstream = { - type: "error", - error: { - type: "invalid_request_error", - code: opaqueIdentifier, - reason: opaqueIdentifier, - message: "quota metadata from /srv/provider/private.json", - credential: "credential-secret", - session_count: 2, - details: [{ type: "integer", reason: "must be positive" }], - }, - }; - - assert.equal(shouldPassthroughUpstreamError(422, upstream), true); - const response = buildPassthroughErrorResponse(422, upstream); - assert.ok(response); - const serialized = JSON.stringify(await response.json()); - - assert.match(serialized, /invalid_request_error/); - assert.match(serialized, /"session_count":2/); - assert.match(serialized, /"type":"integer","reason":"must be positive"/); - assert.doesNotMatch( - serialized, - new RegExp(`credential-secret|srv/provider|${opaqueIdentifier}`, "i") - ); -}); - -test("upstream classification projection preserves HTTP numbers and rejects opaque aliases", () => { - const opaqueIdentifier = "AbC9xY7pQ2mN8vR4kL6z"; - const projected = sanitizeUpstreamDetails({ - code: 400, - status: "UNAVAILABLE", - oversizedCode: 40002, - error: { - code: 40002, - error_code: opaqueIdentifier, - errorCode: opaqueIdentifier, - error_type: opaqueIdentifier, - errorType: opaqueIdentifier, - sub_type: opaqueIdentifier, - subType: opaqueIdentifier, - status: opaqueIdentifier, - status_code: opaqueIdentifier, - statusCode: opaqueIdentifier, - message: "safe provider wording", - }, - }) as { - code?: unknown; - status?: unknown; - oversizedCode?: unknown; - error?: Record; - }; - - assert.equal(projected.code, 400); - assert.equal(projected.status, "UNAVAILABLE"); - assert.equal(projected.oversizedCode, 40002); - assert.equal(projected.error?.code, undefined); - assert.equal(projected.error?.error_code, ""); - assert.equal(projected.error?.errorCode, ""); - assert.equal(projected.error?.error_type, "upstream_error"); - assert.equal(projected.error?.errorType, "upstream_error"); - assert.equal(projected.error?.sub_type, "upstream_error"); - assert.equal(projected.error?.subType, "upstream_error"); - assert.equal(projected.error?.status, undefined); - assert.equal(projected.error?.status_code, undefined); - assert.equal(projected.error?.statusCode, undefined); - assert.equal(projected.error?.message, "safe provider wording"); - assert.doesNotMatch(JSON.stringify(projected), new RegExp(opaqueIdentifier)); -}); - -test("upstream classification projection preserves only real gRPC numeric codes", () => { - const projected = sanitizeUpstreamDetails({ - error: { code: 7 }, - errors: [{ code: 16 }, { code: 17 }, { code: 40002 }], - status: 7, - warning: { code: "model_capacity", type: "unknown" }, - }) as { - error?: { code?: unknown }; - errors?: Array<{ code?: unknown }>; - status?: unknown; - warning?: { code?: unknown; type?: unknown }; - }; - - assert.equal(projected.error?.code, 7); - assert.equal(projected.errors?.[0]?.code, 16); - assert.equal(projected.errors?.[1]?.code, undefined); - assert.equal(projected.errors?.[2]?.code, undefined); - assert.equal(projected.status, undefined); - assert.equal(projected.warning?.code, ""); - assert.equal(projected.warning?.type, "upstream_error"); -}); - -test("sanitizeUpstreamDetails fails closed for hostile prototype access", () => { - const hostile = new Proxy( - {}, - { - getPrototypeOf(): never { - throw new Error("access_token=prototype-secret at /srv/private/prototype.ts"); - }, - } - ); - - let projected: unknown; - assert.doesNotThrow(() => { - projected = sanitizeUpstreamDetails(hostile); - }); - assert.doesNotMatch(JSON.stringify(projected), /prototype-secret|srv\/private|prototype\.ts/i); -}); - -test("upstream passthrough fails closed for non-serializable bodies", () => { - const cyclic: Record = { error: { message: "safe" } }; - cyclic.self = cyclic; - - assert.equal(shouldPassthroughUpstreamError(400, cyclic), false); - assert.equal(buildPassthroughErrorResponse(400, cyclic), null); -}); - -test("upstream passthrough fails closed when getters change after eligibility", () => { - let reads = 0; - const upstream = Object.create(null) as Record; - Object.defineProperty(upstream, "error", { - enumerable: true, - get(): unknown { - reads += 1; - if (reads === 1) return { message: "safe capability error" }; - throw new Error("access_token=second-read-secret at /srv/private/getter.ts:1:2"); - }, - }); - - assert.doesNotThrow(() => buildPassthroughErrorResponse(400, upstream)); - assert.equal(buildPassthroughErrorResponse(400, upstream), null); }); diff --git a/tests/unit/error-sensitive-redaction.test.ts b/tests/unit/error-sensitive-redaction.test.ts index 813db320aa..eb2e7e2392 100644 --- a/tests/unit/error-sensitive-redaction.test.ts +++ b/tests/unit/error-sensitive-redaction.test.ts @@ -1,29 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-error-sensitive-redaction-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { sanitizeErrorMessage, sanitizeUpstreamDetails } = - await import("../../open-sse/utils/errorSanitization.ts"); - -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); +import { + sanitizeErrorMessage, + sanitizeUpstreamDetails, +} from "../../open-sse/utils/errorSanitization.ts"; test("sanitizeErrorMessage removes bearer credentials and image data URLs", () => { const raw = diff --git a/tests/unit/fixtures/error-public-boundaries-hardening.fixture.ts b/tests/unit/fixtures/error-public-boundaries-hardening.fixture.ts new file mode 100644 index 0000000000..2b953ebd9e --- /dev/null +++ b/tests/unit/fixtures/error-public-boundaries-hardening.fixture.ts @@ -0,0 +1,608 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-public-errors-")); +const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); +const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; +const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); + +fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; + +const core = await import("../../../src/lib/db/core.ts"); +const { + buildErrorBody, + buildModelCooldownBody, + createErrorResult, + parseUpstreamError, + projectPublicErrorIdentifier, + providerCircuitOpenResponse, + sanitizeErrorMessage, + sanitizeUpstreamDetails, + unavailableResponse, +} = await import("../../../open-sse/utils/error.ts"); +const { buildPassthroughErrorResponse, shouldPassthroughUpstreamError } = + await import("../../../open-sse/utils/upstreamErrorPassthrough.ts"); + +test.after(() => { + core.resetDbInstance(); + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; + else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; + fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("sanitizeErrorMessage removes non-source paths, credentials, and serialized stacks", () => { + const raw = String.raw`Provider failed at /srv/private/provider-key.json access_token=provider-secret\n at validate (C:\Users\admin\private\validator.ts:42:7)`; + const safe = sanitizeErrorMessage(raw); + + assert.match(safe, /Provider failed/i); + assert.doesNotMatch(safe, /srv\/private|provider-secret|C:\\Users|validator\.ts/i); + assert.doesNotMatch(safe, /\\n\s*at validate/i); +}); + +test("sanitizeErrorMessage redacts Windows drive-root-relative filesystem paths", () => { + const plain = sanitizeErrorMessage( + String.raw`Provider failed at \Users\admin\private\secret.txt` + ); + const quoted = sanitizeErrorMessage( + String.raw`Provider failed opening "\Windows\Temp\native.dll"` + ); + const singleSegment = sanitizeErrorMessage(String.raw`Provider failed opening \private.db`); + const prose = sanitizeErrorMessage(String.raw`Provider reported \offline without a path`); + const escapedInitialPaths = [ + String.raw`Provider failed at \bin\private.db`, + String.raw`Provider failed at \folder\private.db`, + String.raw`Provider failed at \new\private.db`, + String.raw`Provider failed at \root\private.db`, + String.raw`Provider failed at \temp\private.db`, + String.raw`Provider failed at C:\temp\private.db`, + ].map((message) => sanitizeErrorMessage(message)); + + assert.equal(plain, "Provider failed at "); + assert.equal(quoted, 'Provider failed opening ""'); + assert.equal(singleSegment, "Provider failed opening "); + assert.equal(prose, String.raw`Provider reported \offline without a path`); + for (const projected of escapedInitialPaths) { + assert.equal(projected, "Provider failed at "); + } +}); + +test("sanitizeErrorMessage redacts extensionless POSIX paths without hiding explicit routes", () => { + const compact = sanitizeErrorMessage("Provider failed at /custom/internal/secret"); + const spaced = sanitizeErrorMessage("Provider failed at /custom/internal secret directory"); + const route = sanitizeErrorMessage("Route /dashboard/providers is unavailable"); + const singleSegment = sanitizeErrorMessage("Provider failed opening /vault"); + const singleSegmentRoute = sanitizeErrorMessage("Route /vault is unavailable"); + const compoundPathAndRoute = sanitizeErrorMessage( + "Failed /vault then GET /home/profile returned 404" + ); + const knownRootRoutes = [ + sanitizeErrorMessage("GET /home returned 404"), + sanitizeErrorMessage("Route /run is unavailable"), + sanitizeErrorMessage("POST /data returned 409"), + sanitizeErrorMessage("Route /var is unavailable"), + ]; + const body = buildErrorBody(500, "Provider failed at /custom/internal/secret"); + + assert.doesNotMatch(compact, /custom\/internal\/secret/); + assert.doesNotMatch(spaced, /custom\/internal|secret directory/); + assert.doesNotMatch(body.error.message, /custom\/internal\/secret/); + assert.match(compact, //); + assert.equal(route, "Route /dashboard/providers is unavailable"); + assert.equal(singleSegment, "Provider failed opening "); + assert.equal(singleSegmentRoute, "Route /vault is unavailable"); + assert.equal(compoundPathAndRoute, "Failed then GET /home/profile returned 404"); + assert.deepEqual(knownRootRoutes, [ + "GET /home returned 404", + "Route /run is unavailable", + "POST /data returned 409", + "Route /var is unavailable", + ]); +}); + +test("sanitizeErrorMessage fails closed when string coercion is hostile", () => { + const hostile = { + toString(): never { + throw new Error("access_token=hostile-secret at /srv/private/hostile.ts:1:2"); + }, + }; + + assert.equal(sanitizeErrorMessage(hostile), ""); +}); + +test("buildErrorBody projects untrusted error classifications onto safe identifiers", () => { + const body = buildErrorBody(502, "upstream failed", undefined, { + type: "server_error\nX-Leak: yes", + code: "sk-live-secret-value", + reason: "access_token=reason-secret", + }); + + assert.equal(body.error.type, "server_error"); + assert.equal(body.error.code, "bad_gateway"); + assert.equal(body.error.reason, undefined); +}); + +test("createErrorResult rejects opaque upstream identifiers that could be echoed credentials", async () => { + const opaqueCredential = "AbC9xY7pQ2mN8vR4kL6z"; + const result = createErrorResult( + 502, + "upstream failed", + null, + opaqueCredential, + opaqueCredential + ); + const body = (await result.response.json()) as { + error: { code: string; type: string }; + }; + + assert.equal(body.error.code, "bad_gateway"); + assert.equal(body.error.type, "server_error"); + assert.doesNotMatch(JSON.stringify(body), new RegExp(opaqueCredential)); +}); + +test("parseUpstreamError never stringifies an untrusted error object into the public message", async () => { + const opaqueIdentifier = "AbC9xY7pQ2mN8vR4kL6z"; + const parsed = await parseUpstreamError( + Response.json( + { + error: { + code: opaqueIdentifier, + type: opaqueIdentifier, + reason: opaqueIdentifier, + }, + }, + { status: 502 } + ), + "openai" + ); + const result = createErrorResult( + parsed.statusCode, + parsed.message, + parsed.retryAfterMs, + parsed.errorCode as string, + parsed.errorType as string, + parsed.responseBody + ); + const bodyText = await result.response.text(); + + assert.equal(parsed.message, "Upstream error: 502"); + assert.doesNotMatch(bodyText, new RegExp(opaqueIdentifier)); +}); + +test("buildErrorBody preserves the configured empty code for unmapped client statuses", () => { + const body = buildErrorBody(424, "Dependency failed"); + + assert.equal(body.error.type, "invalid_request_error"); + assert.equal(body.error.code, ""); +}); + +test("public identifier vocabulary preserves current internal machine-readable contracts", () => { + const identifiers = [ + "context_length_exceeded", + "tool_calling_not_supported", + "vision", + "tools", + "structured_output", + "context_window", + "unsupported_endpoint", + "unverified_codex_client", + "invalid_previous_response_binding", + "incompatible_reasoning_effort", + "STREAM_READINESS_TIMEOUT", + "stream_timeout", + "STREAM_EARLY_EOF", + "stream_early_eof", + "LEASE_NO_ELIGIBLE_CONNECTION", + "LEASE_ELIGIBILITY_UNAVAILABLE", + "LEASE_UNSUPPORTED_ROUTE", + "LEASE_UNSUPPORTED_TRANSPORT", + "DIRECT_RESPONSE_START_TIMEOUT", + "PROXY_FAMILY_UNAVAILABLE", + "RELAY_TIMEOUT", + "TLS_FINGERPRINT_FAILED", + "PROXY_REQUEST_FAILED", + "TLS_SESSION_CAPACITY", + "TLS_CIRCUIT_OPEN", + "PROVIDER_RETIRED", + "upstream_empty_response", + "upstream_response_error", + "upstream_response_failed", + "stream_pipeline_error", + "stream_terminated", + "rate_limited", + "usage_limit_reached", + "timeout", + "semaphore_timeout", + "semaphore_queue_full", + "RATE_LIMIT_EXECUTION_TIMEOUT", + "RATE_LIMIT_QUEUE_FULL", + "RATE_LIMIT_QUEUE_WEDGED", + "RATE_LIMIT_QUEUE_TIMEOUT", + "rate_limit_queue_wedged", + "429", + "empty_response", + "stream_idle_timeout", + "empty_content", + "UNAVAILABLE", + "RESOURCE_EXHAUSTED", + "provider_unavailable", + "unsupported_feature", + "missing_project_id", + "oauth_missing_project_id", + "gcp_project_required", + "QUOTA_ONLY", + "QUOTA_NOT_ALLOCATED", + "cloudflare_challenge", + "cf_mitigated_challenge", + "upstream_protocol_error", + "claude_web_protocol_error", + "service_not_running", + "storage_encryption_stale", + "HTTP_429", + "BLACKBOX_SUBSCRIPTION_REQUIRED", + "BLACKBOX_AUTH_REQUIRED", + "BLACKBOX_RATE_LIMIT", + "abort", + "ABORTED", + "CHIPOTLE_ERROR", + "premium_model_requires_key", + "GROK_ERROR", + "TLS_CLIENT_UNAVAILABLE", + "upstream_access_denied", + "proxy_unavailable", + "EXECUTOR_ERROR", + "executor_contract_violation", + "orphan_tool_result", + "bedrock_stream_error", + "invalid_kiro_tool_call", + "devin_cli_error", + "upstream_websocket_error", + "upstream_websocket_connect_failed", + "codex_app_server_turn_failed", + "missing_credits", + "reached_limit", + "rate_limit_reached", + "rate_limit_longer_reached", + "client_cancelled", + "client_closed_request", + "compaction_control_unavailable", + "compaction_handoff_failed", + "connector_not_found", + "connector_error", + "prompt_attachment_integrity", + "chatgpt_session_expired", + "chatgpt_subscription_unavailable", + "upstream_server_error", + "multipart_protocol_violation", + "browser_stream_inconsistent", + "structured_output_validation_failed", + "chatgpt_submission_ambiguous", + "chatgpt_submitted_turn_failed", + "cli_not_found", + "upstream_auth_error", + "wreq_unavailable", + "api_error", + "connection_error", + "unsupported_runtime", + "VIDEO_ARTIFACT_URL_INVALID", + "VIDEO_ARTIFACT_URL_BLOCKED", + "VIDEO_ARTIFACT_DOWNLOAD_FAILED", + "VIDEO_ARTIFACT_TOO_LARGE", + "VIDEO_ARTIFACT_SIGNATURE_INVALID", + "VIDEO_ARTIFACT_NOT_READY", + "VIDEO_ARTIFACT_UNAVAILABLE", + "VIDEO_ARTIFACT_CONTENT_TYPE_INVALID", + "codex_app_server_unconfigured", + "meta_ai_warmup_failed", + "meta_ai_mode_switch_failed", + "meta_ai_ws_error", + "meta_ai_empty_response", + "PPLX_ERROR", + "cloudflare_or_bot", + "request_failed", + "lmarena_error", + "network_error", + ]; + + for (const identifier of identifiers) { + assert.equal(projectPublicErrorIdentifier(identifier, "bad_request"), identifier, identifier); + } +}); + +test("public numeric identifiers are limited to three-digit HTTP status codes", () => { + assert.equal(projectPublicErrorIdentifier("100", "bad_request"), "100"); + assert.equal(projectPublicErrorIdentifier("599", "bad_request"), "599"); + assert.equal(projectPublicErrorIdentifier("099", "bad_request"), "bad_request"); + assert.equal(projectPublicErrorIdentifier("600", "bad_request"), "bad_request"); + assert.equal(projectPublicErrorIdentifier("5000", "bad_request"), "bad_request"); + assert.equal(projectPublicErrorIdentifier("40002", "bad_request"), "bad_request"); + assert.equal(projectPublicErrorIdentifier("HTTP_600", "bad_request"), "bad_request"); + assert.equal(projectPublicErrorIdentifier("HTTP_40002", "bad_request"), "bad_request"); + assert.equal(projectPublicErrorIdentifier("weird_error", "bad_gateway"), "bad_gateway"); +}); + +test("buildErrorBody callers never overwrite a projected public classification", () => { + const productionFiles: string[] = []; + const collectTypeScriptFiles = (directory: string): void => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + if (entry.name === "__tests__") continue; + collectTypeScriptFiles(entryPath); + } else if (entry.isFile() && /\.tsx?$/.test(entry.name)) { + productionFiles.push(entryPath); + } + } + }; + + collectTypeScriptFiles(path.join(REPO_ROOT, "open-sse")); + collectTypeScriptFiles(path.join(REPO_ROOT, "src")); + + const mutationPattern = /\b[A-Za-z_$][A-Za-z0-9_$]*\.error\.(?:code|type|reason)\s*=(?!=)/g; + const violations: string[] = []; + for (const filePath of productionFiles) { + const source = fs.readFileSync(filePath, "utf8"); + if (!source.includes("buildErrorBody")) continue; + for (const match of source.matchAll(mutationPattern)) { + const line = source.slice(0, match.index).split("\n").length; + violations.push(`${path.relative(REPO_ROOT, filePath)}:${line}`); + } + } + + assert.deepEqual(violations, []); + + const chatCoreSource = fs.readFileSync( + path.join(REPO_ROOT, "open-sse/handlers/chatCore.ts"), + "utf8" + ); + assert.doesNotMatch( + chatCoreSource, + /JSON\.stringify\(\s*\{\s*error\s*:\s*\{/, + "chatCore must not bypass buildErrorBody with a manually assembled error envelope" + ); +}); + +test("operational log persistence catches use the canonical sanitizer", () => { + const callLogsSource = fs.readFileSync(path.join(REPO_ROOT, "src/lib/usage/callLogs.ts"), "utf8"); + const proxyLoggerSource = fs.readFileSync(path.join(REPO_ROOT, "src/lib/proxyLogger.ts"), "utf8"); + + assert.match(callLogsSource, /sanitizeErrorMessage\(error\)/); + assert.doesNotMatch(callLogsSource, /\(error as Error\)\.message/); + assert.match(proxyLoggerSource, /sanitizeErrorMessage\(err\)/); + assert.doesNotMatch(proxyLoggerSource, /err\?\.message\s*\|\|\s*err/); +}); + +test("stream request finalization never warns with a raw error object", () => { + const source = fs.readFileSync( + path.join(REPO_ROOT, "open-sse/utils/streamFailureFinalization.ts"), + "utf8" + ); + + assert.match(source, /sanitizeErrorMessage\(error\)/); + assert.doesNotMatch(source, /"message" in error[\s\S]{0,160}: error/); +}); + +test("chatCore provider-failure writes use the projected persistent message", () => { + const source = fs.readFileSync(path.join(REPO_ROOT, "open-sse/handlers/chatCore.ts"), "utf8"); + const failureStart = source.indexOf("providerFailure: if (!providerResponse.ok)"); + const failureEnd = source.indexOf("// Non-streaming response", failureStart); + assert.ok(failureStart >= 0 && failureEnd > failureStart, "providerFailure block must exist"); + const failureBlock = source.slice(failureStart, failureEnd); + + assert.doesNotMatch(failureBlock, /lastError:\s*message\b/); + assert.ok( + (failureBlock.match(/lastError:\s*persistentMessage\b/g) || []).length >= 11, + "every providerFailure persistence branch must use persistentMessage" + ); +}); + +test("public cooldown and circuit responses sanitize dynamic context", async () => { + const unavailable = unavailableResponse( + 503, + "Provider failed at /srv/private/state.sqlite access_token=unavailable-secret", + 5, + "retry after reading C:\\Users\\admin\\private\\state.json" + ); + const unavailableBody = (await unavailable.json()) as { error: { message: string } }; + assert.doesNotMatch(unavailableBody.error.message, /srv\/private|unavailable-secret|C:\\Users/i); + + const circuit = providerCircuitOpenResponse( + "provider access_token=circuit-secret /home/service/provider.json", + 5 + ); + const circuitBody = (await circuit.json()) as { + error: { message: string; provider: string }; + }; + assert.equal(circuitBody.error.provider, "unknown"); + assert.doesNotMatch(JSON.stringify(circuitBody), /circuit-secret|\/home\/service/i); + + const cooldown = buildModelCooldownBody({ + model: "model access_token=model-secret /opt/models/private.json", + retryAfterSec: Number.NaN, + retryAfterAt: "not-a-timestamp access_token=timestamp-secret", + }); + assert.equal(cooldown.error.model, undefined); + assert.equal(cooldown.error.retry_after, undefined); + assert.equal(cooldown.error.reset_seconds, 1); + assert.doesNotMatch(JSON.stringify(cooldown), /model-secret|timestamp-secret|\/opt\/models/i); +}); + +test("sanitizeUpstreamDetails drops credential aliases and prototype-control keys", () => { + const input = Object.create(null) as Record; + input.error = { + message: "quota metadata at /srv/provider/private.json", + credential: "credential-secret", + sessionId: "session-secret", + session_count: 2, + }; + input.__proto__ = { leaked: true }; + + const safe = sanitizeUpstreamDetails(input) as Record; + const serialized = JSON.stringify(safe); + + assert.doesNotMatch(serialized, /credential-secret|session-secret|srv\/provider|__proto__/i); + assert.match(serialized, /"session_count":2/); +}); + +test("buildErrorBody fails closed for hostile upstream detail accessors", () => { + const hostile = new Proxy( + {}, + { + ownKeys(): never { + throw new Error("access_token=hostile-detail at /srv/private/detail.ts:1:2"); + }, + } + ); + + let body: ReturnType | undefined; + assert.doesNotThrow(() => { + body = buildErrorBody(502, "upstream failed", hostile); + }); + assert.equal(body?.upstream_details, undefined); + assert.doesNotMatch(JSON.stringify(body), /hostile-detail|srv\/private|detail\.ts/i); +}); + +test("upstream passthrough preserves safe wording but recursively sanitizes the JSON body", async () => { + const opaqueIdentifier = "AbC9xY7pQ2mN8vR4kL6z"; + const upstream = { + type: "error", + error: { + type: "invalid_request_error", + code: opaqueIdentifier, + reason: opaqueIdentifier, + message: "quota metadata from /srv/provider/private.json", + credential: "credential-secret", + session_count: 2, + details: [{ type: "integer", reason: "must be positive" }], + }, + }; + + assert.equal(shouldPassthroughUpstreamError(422, upstream), true); + const response = buildPassthroughErrorResponse(422, upstream); + assert.ok(response); + const serialized = JSON.stringify(await response.json()); + + assert.match(serialized, /invalid_request_error/); + assert.match(serialized, /"session_count":2/); + assert.match(serialized, /"type":"integer","reason":"must be positive"/); + assert.doesNotMatch( + serialized, + new RegExp(`credential-secret|srv/provider|${opaqueIdentifier}`, "i") + ); +}); + +test("upstream classification projection preserves HTTP numbers and rejects opaque aliases", () => { + const opaqueIdentifier = "AbC9xY7pQ2mN8vR4kL6z"; + const projected = sanitizeUpstreamDetails({ + code: 400, + status: "UNAVAILABLE", + oversizedCode: 40002, + error: { + code: 40002, + error_code: opaqueIdentifier, + errorCode: opaqueIdentifier, + error_type: opaqueIdentifier, + errorType: opaqueIdentifier, + sub_type: opaqueIdentifier, + subType: opaqueIdentifier, + status: opaqueIdentifier, + status_code: opaqueIdentifier, + statusCode: opaqueIdentifier, + message: "safe provider wording", + }, + }) as { + code?: unknown; + status?: unknown; + oversizedCode?: unknown; + error?: Record; + }; + + assert.equal(projected.code, 400); + assert.equal(projected.status, "UNAVAILABLE"); + assert.equal(projected.oversizedCode, 40002); + assert.equal(projected.error?.code, undefined); + assert.equal(projected.error?.error_code, ""); + assert.equal(projected.error?.errorCode, ""); + assert.equal(projected.error?.error_type, "upstream_error"); + assert.equal(projected.error?.errorType, "upstream_error"); + assert.equal(projected.error?.sub_type, "upstream_error"); + assert.equal(projected.error?.subType, "upstream_error"); + assert.equal(projected.error?.status, undefined); + assert.equal(projected.error?.status_code, undefined); + assert.equal(projected.error?.statusCode, undefined); + assert.equal(projected.error?.message, "safe provider wording"); + assert.doesNotMatch(JSON.stringify(projected), new RegExp(opaqueIdentifier)); +}); + +test("upstream classification projection preserves only real gRPC numeric codes", () => { + const projected = sanitizeUpstreamDetails({ + error: { code: 7 }, + errors: [{ code: 16 }, { code: 17 }, { code: 40002 }], + status: 7, + warning: { code: "model_capacity", type: "unknown" }, + }) as { + error?: { code?: unknown }; + errors?: Array<{ code?: unknown }>; + status?: unknown; + warning?: { code?: unknown; type?: unknown }; + }; + + assert.equal(projected.error?.code, 7); + assert.equal(projected.errors?.[0]?.code, 16); + assert.equal(projected.errors?.[1]?.code, undefined); + assert.equal(projected.errors?.[2]?.code, undefined); + assert.equal(projected.status, undefined); + assert.equal(projected.warning?.code, ""); + assert.equal(projected.warning?.type, "upstream_error"); +}); + +test("sanitizeUpstreamDetails fails closed for hostile prototype access", () => { + const hostile = new Proxy( + {}, + { + getPrototypeOf(): never { + throw new Error("access_token=prototype-secret at /srv/private/prototype.ts"); + }, + } + ); + + let projected: unknown; + assert.doesNotThrow(() => { + projected = sanitizeUpstreamDetails(hostile); + }); + assert.doesNotMatch(JSON.stringify(projected), /prototype-secret|srv\/private|prototype\.ts/i); +}); + +test("upstream passthrough fails closed for non-serializable bodies", () => { + const cyclic: Record = { error: { message: "safe" } }; + cyclic.self = cyclic; + + assert.equal(shouldPassthroughUpstreamError(400, cyclic), false); + assert.equal(buildPassthroughErrorResponse(400, cyclic), null); +}); + +test("upstream passthrough fails closed when getters change after eligibility", () => { + let reads = 0; + const upstream = Object.create(null) as Record; + Object.defineProperty(upstream, "error", { + enumerable: true, + get(): unknown { + reads += 1; + if (reads === 1) return { message: "safe capability error" }; + throw new Error("access_token=second-read-secret at /srv/private/getter.ts:1:2"); + }, + }); + + assert.doesNotThrow(() => buildPassthroughErrorResponse(400, upstream)); + assert.equal(buildPassthroughErrorResponse(400, upstream), null); +}); diff --git a/tests/unit/fixtures/mcp-public-error-boundaries.fixture.ts b/tests/unit/fixtures/mcp-public-error-boundaries.fixture.ts new file mode 100644 index 0000000000..1eaad34050 --- /dev/null +++ b/tests/unit/fixtures/mcp-public-error-boundaries.fixture.ts @@ -0,0 +1,184 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mcp-error-boundaries-")); +const repoRoot = fileURLToPath(new URL("../../..", import.meta.url)); +const originalDataDir = process.env.DATA_DIR; +const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; +const originalApiKey = process.env.OMNIROUTE_API_KEY; +const originalApiKeyId = process.env.OMNIROUTE_API_KEY_ID; +const originalInternalToken = process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN; +const originalInternalTokenFile = process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE; +const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; +process.env.DATA_DIR = path.join(testRoot, "data"); +process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); +process.env.OMNIROUTE_API_KEY = "mcp-boundary-test-key"; +process.env.OMNIROUTE_API_KEY_ID = "mcp-boundary-test-key-id"; +process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN = "mcp-boundary-internal-test-token"; +process.env.OMNIROUTE_BASE_URL = "http://localhost:20128"; +delete process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE; +fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); +fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); + +const { createMcpServer } = await import("../../../open-sse/mcp-server/server.ts"); +const { closeAuditDb, queryAuditEntries } = await import("../../../open-sse/mcp-server/audit.ts"); +const { obsidianTools } = await import("../../../open-sse/mcp-server/tools/obsidianTools.ts"); +const { skillTools } = await import("../../../open-sse/mcp-server/tools/skillTools.ts"); +const { skillRegistry } = await import("../../../src/lib/skills/registry.ts"); +const { skillExecutor } = await import("../../../src/lib/skills/executor.ts"); +const core = await import("../../../src/lib/db/core.ts"); + +type McpResult = { + content?: Array<{ type: string; text: string }>; + isError?: boolean; +}; + +type RegisteredTool = { + handler: (args: unknown, extra?: unknown) => Promise; +}; + +function getRegisteredHandler(server: unknown, toolName: string): RegisteredTool["handler"] { + const registry = (server as { _registeredTools?: Record }) + ._registeredTools; + assert.ok(registry, "McpServer should expose _registeredTools"); + const tool = registry[toolName]; + assert.ok(tool, `${toolName} must be registered`); + return tool.handler; +} + +function assertPublicMcpError(result: McpResult): void { + const text = result.content?.[0]?.text ?? ""; + assert.equal(result.isError, true); + assert.match(text, /Error:/); + assert.doesNotMatch(text, /mcp-boundary-secret|srv\/private|mcp-boundary\.ts|\bat execute\b/i); +} + +test.after(() => { + closeAuditDb(); + core.resetDbInstance(); + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; + if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; + else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; + if (originalApiKey === undefined) delete process.env.OMNIROUTE_API_KEY; + else process.env.OMNIROUTE_API_KEY = originalApiKey; + if (originalApiKeyId === undefined) delete process.env.OMNIROUTE_API_KEY_ID; + else process.env.OMNIROUTE_API_KEY_ID = originalApiKeyId; + if (originalInternalToken === undefined) delete process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN; + else process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN = originalInternalToken; + if (originalInternalTokenFile === undefined) { + delete process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE; + } else { + process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE = originalInternalTokenFile; + } + if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; + else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; + fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("core MCP handlers sanitize upstream bodies before public and audit boundaries", async () => { + const hostile = "Bearer mcp-fetch-boundary-secret at /srv/private/mcp-fetch-boundary.ts:9:3"; + const originalFetch = globalThis.fetch; + const calledUrls: string[] = []; + globalThis.fetch = async (input) => { + calledUrls.push(String(input)); + return new Response(hostile, { status: 500 }); + }; + + try { + const handler = getRegisteredHandler(createMcpServer(), "omniroute_list_combos"); + const result = await handler({ includeMetrics: false }); + const publicText = result.content?.[0]?.text ?? ""; + assert.equal(result.isError, true); + assert.doesNotMatch( + publicText, + /mcp-fetch-boundary-secret|srv\/private|mcp-fetch-boundary\.ts/i + ); + assert.deepEqual(calledUrls, ["http://localhost:20128/api/combos"]); + + const audit = await queryAuditEntries({ tool: "omniroute_list_combos", success: false }); + assert.ok(audit.entries.length >= 1); + assert.doesNotMatch( + JSON.stringify(audit.entries), + /mcp-fetch-boundary-secret|srv\/private|mcp-fetch-boundary\.ts/i + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("every MCP public catch uses the canonical fail-closed projector", () => { + const source = fs.readFileSync(path.join(repoRoot, "open-sse/mcp-server/server.ts"), "utf8"); + assert.doesNotMatch(source, /err instanceof Error \? err\.message : String\(err\)/); +}); + +test("Obsidian and dynamic-skill MCP wrappers sanitize thrown errors", async () => { + const hostile = new Error( + "MCP failed access_token=mcp-boundary-secret at /srv/private/mcp-boundary.ts\n" + + " at execute (/srv/private/mcp-boundary.ts:9:3)" + ); + const mutableObsidianTool = obsidianTools[0] as unknown as { + name: string; + handler: (args: unknown, extra?: unknown) => Promise; + }; + const originalObsidianHandler = mutableObsidianTool.handler; + try { + mutableObsidianTool.handler = async () => { + throw hostile; + }; + const obsidianHandler = getRegisteredHandler(createMcpServer(), mutableObsidianTool.name); + assertPublicMcpError(await obsidianHandler({}, { authInfo: { scopes: ["read:obsidian"] } })); + } finally { + mutableObsidianTool.handler = originalObsidianHandler; + } + + const mutableRegistry = skillRegistry as unknown as { + list: () => Array<{ name: string; description: string; enabled: boolean }>; + }; + const mutableExecutor = skillExecutor as unknown as { + execute: (...args: unknown[]) => Promise; + }; + const originalList = mutableRegistry.list; + const originalExecute = mutableExecutor.execute; + try { + mutableRegistry.list = () => [ + { name: "mcp_boundary_skill", description: "boundary test", enabled: true }, + ]; + const dynamicHandler = getRegisteredHandler(createMcpServer(), "skill_mcp_boundary_skill"); + mutableExecutor.execute = async () => { + throw hostile; + }; + assertPublicMcpError( + await dynamicHandler({}, { authInfo: { clientId: "test", scopes: ["execute:skills"] } }) + ); + } finally { + mutableRegistry.list = originalList; + mutableExecutor.execute = originalExecute; + } +}); + +test("skill-tool MCP wrapper uses its own fail-closed fallback for hostile thrown values", async () => { + const mutableSkillTool = Object.values(skillTools)[0] as unknown as { + name: string; + handler: (args: unknown, extra?: unknown) => Promise; + }; + const originalHandler = mutableSkillTool.handler; + const revocable = Proxy.revocable({}, {}); + revocable.revoke(); + + try { + mutableSkillTool.handler = async () => { + throw revocable.proxy; + }; + const handler = getRegisteredHandler(createMcpServer(), mutableSkillTool.name); + const result = await handler({}, { authInfo: { scopes: ["read:skills"] } }); + assert.equal(result.isError, true); + assert.equal(result.content?.[0]?.text, "Error: Skill tool execution failed"); + } finally { + mutableSkillTool.handler = originalHandler; + } +}); diff --git a/tests/unit/fixtures/provider-connection-test-error-boundaries.fixture.ts b/tests/unit/fixtures/provider-connection-test-error-boundaries.fixture.ts new file mode 100644 index 0000000000..3ab084243b --- /dev/null +++ b/tests/unit/fixtures/provider-connection-test-error-boundaries.fixture.ts @@ -0,0 +1,262 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-provider-errors-")); +const originalDataDir = process.env.DATA_DIR; +const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; +const originalApiKeySecret = process.env.API_KEY_SECRET; +const originalDisableBackup = process.env.DISABLE_SQLITE_AUTO_BACKUP; +const originalDisableHealthCheck = process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK; +const pluginsDir = path.join(testRoot, "plugins"); +const testDataDir = path.join(testRoot, "data"); +fs.mkdirSync(pluginsDir, { recursive: true }); +fs.mkdirSync(testDataDir, { recursive: true }); +process.env.OMNIROUTE_PLUGINS_DIR = pluginsDir; +process.env.DATA_DIR = testDataDir; +assert.notEqual(fs.realpathSync(testDataDir), "/home/diegosouzapw/.omniroute"); +assert.notEqual(fs.realpathSync(pluginsDir), "/home/diegosouzapw/.omniroute/plugins"); + +process.env.API_KEY_SECRET = "provider-error-boundary-test-secret"; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK = "true"; + +// Connection tests suppress their call-log entry under node --test. This file +// exercises the real persistent boundary, so present a normal runtime identity +// before importing the route and its logging modules. +const originalArgv = process.argv; +const originalExecArgv = process.execArgv; +const originalNodeEnv = process.env.NODE_ENV; +const originalVitest = process.env.VITEST; +process.argv = [ + process.execPath, + path.join(process.cwd(), "scripts/ad-hoc/omniroute-boundary-harness.mjs"), +]; +process.execArgv = []; +process.env.NODE_ENV = "development"; +delete process.env.VITEST; + +const hostileValidationMessage = + "Jules failed access_token=jules-boundary-secret at /srv/private/validator.ts\n" + + " at probe (/srv/private/validator.ts:42:7)"; +const julesValidationUrl = "https://jules.googleapis.com/v1alpha/sources"; +const originalFetch = globalThis.fetch; +let validationFetchCalls = 0; +const boundaryFetch = (async (input: string | URL | Request) => { + const url = + typeof input === "string" ? input : input instanceof Request ? input.url : input.toString(); + assert.equal(url, julesValidationUrl, `unexpected outbound request: ${url}`); + validationFetchCalls += 1; + return new Response(hostileValidationMessage, { status: 500 }); +}) as typeof fetch; +globalThis.fetch = boundaryFetch; + +const core = await import("../../../src/lib/db/core.ts"); +const providersDb = await import("../../../src/lib/db/providers.ts"); +const { saveCallLog, waitForCallLogSaves, closeCallLogSaves } = + await import("../../../src/lib/usage/callLogs.ts"); +const { flushProxyLogsSync } = await import("../../../src/lib/proxyLogger.ts"); +const { projectProviderRuntimeForPublicResponse, testSingleConnection } = + await import("../../../src/app/api/providers/[id]/test/route.ts"); +// proxyFetch installs its global dispatcher while the imports above load. Put +// the deterministic stub back at the final fetch seam so this test can never +// reach Jules over the network. +globalThis.fetch = boundaryFetch; + +type ArtifactRow = { artifact_relpath: string | null; error_summary: string | null }; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function readArtifact(relativePath: string | null): Record { + assert.ok(relativePath, "call log must have a persisted detail artifact"); + const absolutePath = path.join(testDataDir, "call_logs", relativePath); + return JSON.parse(fs.readFileSync(absolutePath, "utf8")) as Record; +} + +test.after(async () => { + await closeCallLogSaves(2_000); + flushProxyLogsSync(); + globalThis.fetch = originalFetch; + process.argv = originalArgv; + process.execArgv = originalExecArgv; + if (originalNodeEnv === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = originalNodeEnv; + if (originalVitest === undefined) delete process.env.VITEST; + else process.env.VITEST = originalVitest; + if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; + else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; + core.resetDbInstance(); + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; + if (originalApiKeySecret === undefined) delete process.env.API_KEY_SECRET; + else process.env.API_KEY_SECRET = originalApiKeySecret; + if (originalDisableBackup === undefined) delete process.env.DISABLE_SQLITE_AUTO_BACKUP; + else process.env.DISABLE_SQLITE_AUTO_BACKUP = originalDisableBackup; + if (originalDisableHealthCheck === undefined) { + delete process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK; + } else { + process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK = originalDisableHealthCheck; + } + fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("public runtime projection omits host paths and internal error envelopes", () => { + const projected = projectProviderRuntimeForPublicResponse({ + installed: true, + runnable: false, + requiresBinary: true, + reason: "not_executable", + runtimeMode: "local", + version: "v1 from /srv/private/bin/tool", + command: "/srv/private/bin/tool", + commandPath: "/srv/private/bin/tool", + settingsPath: "C:\\Users\\admin\\.config\\tool.json", + error: "access_token=runtime-secret at /srv/private/runtime.json", + diagnosis: { message: "runtime-secret at /srv/private/runtime.ts" }, + }); + const serialized = JSON.stringify(projected); + + assert.equal(projected?.installed, true); + assert.equal(projected?.runnable, false); + assert.equal("commandPath" in (projected || {}), false); + assert.equal("settingsPath" in (projected || {}), false); + assert.equal("error" in (projected || {}), false); + assert.equal("diagnosis" in (projected || {}), false); + assert.doesNotMatch(serialized, /runtime-secret|srv\/private|C:\\\\Users/i); +}); + +test("connection validation projects hostile errors before public and persistent boundaries", async () => { + const connection = await providersDb.createProviderConnection({ + provider: "jules", + authType: "apikey", + name: "Jules Error Boundary", + apiKey: "jules-test-key", + isActive: true, + testStatus: "active", + }); + assert.ok(connection?.id); + + const result = await testSingleConnection(connection.id); + assert.equal(result.valid, false); + assert.ok(validationFetchCalls > 0, "the deterministic Jules stub must handle the probe"); + assert.match(String(result.error), /Jules failed/i); + assert.equal(await waitForCallLogSaves(10_000), true, "call-log write must drain"); + flushProxyLogsSync(); + + const db = core.getDbInstance(); + const providerRow = db + .prepare("SELECT last_error FROM provider_connections WHERE id = ?") + .get(connection.id) as { last_error: string | null }; + const callLogRow = db + .prepare( + `SELECT error_summary, artifact_relpath + FROM call_logs + WHERE connection_id = ? AND model = 'connection-test' + ORDER BY rowid DESC LIMIT 1` + ) + .get(connection.id) as ArtifactRow; + const proxyLogRow = db + .prepare( + `SELECT error + FROM proxy_logs + WHERE connection_id = ? AND provider = 'jules' + AND target_url = 'jules/connection-test' + ORDER BY rowid DESC LIMIT 1` + ) + .get(connection.id) as { error: string | null }; + assert.ok(callLogRow, "connection test must write call_logs"); + assert.ok(proxyLogRow, "connection test must write proxy_logs"); + const artifact = readArtifact(callLogRow.artifact_relpath); + + const boundaries = { + publicResult: result, + providerLastError: providerRow.last_error, + callLogSummary: callLogRow.error_summary, + callLogArtifactError: artifact.error, + proxyLogError: proxyLogRow.error, + }; + const leakPattern = /jules-boundary-secret|srv\/private|validator\.ts|\bat probe\b/i; + const leakingBoundaries = Object.entries(boundaries) + .filter(([, value]) => leakPattern.test(JSON.stringify(value))) + .map(([name]) => name); + assert.deepEqual(leakingBoundaries, []); +}); + +test("failed call logs sanitize response-body copies while successful bodies stay unchanged", async () => { + const hostileBody = { + message: "access_token=call-body-secret at /srv/private/upstream.json", + detail: "Error: api_key=call-detail-secret\n at dispatch (/srv/private/rerank.ts:7:2)", + }; + const successBody = { + message: "Successful output mentions /tmp/public-example.ts and remains unchanged", + usage: { total_tokens: 4 }, + }; + + await saveCallLog({ + id: "error-body-json", + status: 502, + provider: "rerank-test", + model: "rerank-test", + responseBody: hostileBody, + pipelinePayloads: { + providerResponse: { body: hostileBody }, + clientResponse: { body: hostileBody }, + }, + }); + await saveCallLog({ + id: "error-body-text", + status: 503, + provider: "rerank-test", + model: "rerank-test", + responseBody: "Bearer plaintext-body-secret at C:\\Users\\admin\\upstream.txt", + }); + await saveCallLog({ + id: "success-body-control", + status: 200, + provider: "rerank-test", + model: "rerank-test", + responseBody: successBody, + pipelinePayloads: { + providerResponse: { body: successBody }, + clientResponse: { body: successBody }, + }, + }); + await saveCallLog({ + id: "error-body-binary", + status: 500, + provider: "rerank-test", + model: "rerank-test", + responseBody: Buffer.from([1, 2, 3, 4]), + }); + assert.equal(await waitForCallLogSaves(2_000), true, "call-log writes must drain"); + + const db = core.getDbInstance(); + const rows = db + .prepare( + `SELECT id, artifact_relpath FROM call_logs + WHERE id IN ( + 'error-body-json', 'error-body-text', 'success-body-control', 'error-body-binary' + )` + ) + .all() as Array<{ id: string; artifact_relpath: string | null }>; + const artifacts = Object.fromEntries( + rows.map((row) => [row.id, readArtifact(row.artifact_relpath)]) + ) as Record>; + + assert.doesNotMatch( + JSON.stringify({ json: artifacts["error-body-json"], text: artifacts["error-body-text"] }), + /call-body-secret|call-detail-secret|plaintext-body-secret|srv\/private|C:\\\\Users|\bat dispatch\b/i + ); + assert.deepEqual(artifacts["success-body-control"].responseBody, successBody); + assert.equal(artifacts["error-body-binary"].responseBody, "[binary 4 bytes]"); + const pipeline = artifacts["success-body-control"].pipeline; + assert.ok(isRecord(pipeline)); + assert.ok(isRecord(pipeline.providerResponse)); + assert.ok(isRecord(pipeline.clientResponse)); + assert.deepEqual(pipeline.providerResponse.body, successBody); + assert.deepEqual(pipeline.clientResponse.body, successBody); +}); diff --git a/tests/unit/fixtures/provider-last-error-sanitization.fixture.ts b/tests/unit/fixtures/provider-last-error-sanitization.fixture.ts new file mode 100644 index 0000000000..b131bdc67d --- /dev/null +++ b/tests/unit/fixtures/provider-last-error-sanitization.fixture.ts @@ -0,0 +1,109 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-provider-last-error-")); +const testDataDir = path.join(testRoot, "data"); +const testPluginsDir = path.join(testRoot, "plugins"); +const originalEnv = { + DATA_DIR: process.env.DATA_DIR, + OMNIROUTE_PLUGINS_DIR: process.env.OMNIROUTE_PLUGINS_DIR, + API_KEY_SECRET: process.env.API_KEY_SECRET, + DISABLE_SQLITE_AUTO_BACKUP: process.env.DISABLE_SQLITE_AUTO_BACKUP, +}; +fs.mkdirSync(testDataDir, { recursive: true }); +fs.mkdirSync(testPluginsDir, { recursive: true }); +process.env.DATA_DIR = testDataDir; +process.env.OMNIROUTE_PLUGINS_DIR = testPluginsDir; +process.env.API_KEY_SECRET = "provider-last-error-test-secret"; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; + +const core = await import("../../../src/lib/db/core.ts"); +const providersDb = await import("../../../src/lib/db/providers.ts"); +const loggerResource = await import("../../../src/shared/utils/loggerResource.ts"); +const { runAsProbe } = await import("../../../src/shared/utils/probeOrigin.ts"); +const { writeTerminalStatus } = await import("../../../src/shared/utils/terminalStatus.ts"); +const { markAccountUnavailable } = await import("../../../src/sse/services/auth.ts"); + +function restoreEnv(name: keyof typeof originalEnv): void { + const original = originalEnv[name]; + if (original === undefined) delete process.env[name]; + else process.env[name] = original; +} + +function readLastError(connectionId: string): string | null { + const row = core + .getDbInstance() + .prepare("SELECT last_error FROM provider_connections WHERE id = ?") + .get(connectionId) as { last_error: string | null } | undefined; + return row?.last_error ?? null; +} + +test.after(async () => { + core.resetDbInstance(); + await loggerResource.closeSharedLoggerResource(); + restoreEnv("DATA_DIR"); + restoreEnv("OMNIROUTE_PLUGINS_DIR"); + restoreEnv("API_KEY_SECRET"); + restoreEnv("DISABLE_SQLITE_AUTO_BACKUP"); + fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("normal and probe failures sanitize provider_connections.lastError at the write seam", async () => { + const hostile = + "provider failed access_token=provider-last-error-secret at /srv/private/provider.ts\n" + + " at dispatch (/srv/private/provider.ts:12:4)"; + const normal = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "normal last-error boundary", + apiKey: "normal-last-error-test-key", // pragma: allowlist secret + isActive: true, + testStatus: "active", + }); + const probe = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "probe last-error boundary", + apiKey: "probe-last-error-test-key", // pragma: allowlist secret + isActive: true, + testStatus: "active", + }); + const terminal = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "terminal last-error boundary", + apiKey: "terminal-last-error-test-key", // pragma: allowlist secret + isActive: true, + testStatus: "active", + }); + + await markAccountUnavailable(normal.id, 500, hostile, "openai"); + await runAsProbe(() => markAccountUnavailable(probe.id, 500, hostile, "openai")); + await writeTerminalStatus( + terminal.id, + { + testStatus: "banned", + isActive: false, + lastError: hostile, + lastErrorType: "forbidden", + errorCode: "403", + }, + "production" + ); + + const persisted = { + normal: readLastError(normal.id), + probe: readLastError(probe.id), + terminal: readLastError(terminal.id), + }; + assert.match(String(persisted.normal), /provider failed/i); + assert.match(String(persisted.probe), /provider failed/i); + assert.match(String(persisted.terminal), /provider failed/i); + assert.doesNotMatch( + JSON.stringify(persisted), + /provider-last-error-secret|srv\/private|provider\.ts|\bat dispatch\b/i + ); +}); diff --git a/tests/unit/fixtures/request-log-management-boundary.fixture.ts b/tests/unit/fixtures/request-log-management-boundary.fixture.ts new file mode 100644 index 0000000000..e3b75b22a9 --- /dev/null +++ b/tests/unit/fixtures/request-log-management-boundary.fixture.ts @@ -0,0 +1,108 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-log-management-boundary-")); +const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); +const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; +const ORIGINAL_DISABLE_BACKUP = process.env.DISABLE_SQLITE_AUTO_BACKUP; + +fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "1"; + +const core = await import("../../../src/lib/db/core.ts"); +const usageHistory = await import("../../../src/lib/usage/usageHistory.ts"); +const logsRoute = await import("../../../src/app/api/logs/[id]/route.ts"); +const usageHistoryRoute = await import("../../../src/app/api/usage/history/route.ts"); + +test.afterEach(() => { + usageHistory.clearPendingRequests(); +}); + +test.after(() => { + usageHistory.clearPendingRequests(); + core.resetDbInstance(); + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; + else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; + if (ORIGINAL_DISABLE_BACKUP === undefined) delete process.env.DISABLE_SQLITE_AUTO_BACKUP; + else process.env.DISABLE_SQLITE_AUTO_BACKUP = ORIGINAL_DISABLE_BACKUP; + fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +const HOSTILE = + "Bearer management-cache-secret at /srv/private/completed-request.ts:12:3\n" + + " at finalize (/srv/private/finalize.ts:4:2)"; + +async function readManagementDetail(id: string): Promise> { + const response = await logsRoute.GET(undefined as unknown as Request, { params: { id } }); + assert.equal(response.status, 200); + return (await response.json()) as Record; +} + +function serializedDetail(detail: Record): string { + return JSON.stringify(detail); +} + +test("management detail sanitizes in-flight failure chunks at the endpoint boundary", async () => { + const requestId = usageHistory.trackPendingRequest("model", "provider", "conn-inflight", true); + assert.ok(requestId); + usageHistory.updatePendingRequestStreamChunks("model", "provider", "conn-inflight", { + provider: [`event: error\ndata: ${HOSTILE}\n\n`], + openai: [], + client: [], + }); + + const detail = await readManagementDetail(requestId); + assert.doesNotMatch( + serializedDetail(detail), + /management-cache-secret|srv\/private|completed-request\.ts|\bat finalize\b/i + ); +}); + +test("management detail sanitizes completed error metadata and cached chunks", async () => { + const requestId = usageHistory.trackPendingRequest("model", "provider", "conn-completed", true); + assert.ok(requestId); + usageHistory.updatePendingRequestStreamChunks("model", "provider", "conn-completed", { + provider: [`data: ${JSON.stringify({ type: "error", message: HOSTILE })}\n\n`], + openai: [], + client: [], + }); + assert.equal( + usageHistory.finalizePendingRequestById(requestId, { status: 502, error: HOSTILE }), + true + ); + + const detail = await readManagementDetail(requestId); + assert.doesNotMatch( + serializedDetail(detail), + /management-cache-secret|srv\/private|completed-request\.ts|\bat finalize\b/i + ); +}); + +test("usage history endpoint exposes pending counters without raw request details", async () => { + const requestId = usageHistory.trackPendingRequest("model", "provider", "conn-usage", true); + assert.ok(requestId); + usageHistory.updatePendingRequestStreamChunks("model", "provider", "conn-usage", { + provider: [`event: error\ndata: ${HOSTILE}\n\n`], + openai: [], + client: [], + }); + + const response = await usageHistoryRoute.GET(undefined as unknown as Request); + assert.equal(response.status, 200); + const body = (await response.json()) as { + pending?: { byModel?: Record; details?: unknown }; + }; + assert.equal(body.pending?.byModel?.["model (provider)"], 1); + assert.equal("details" in (body.pending ?? {}), false); + assert.doesNotMatch(JSON.stringify(body), /management-cache-secret|srv\/private/i); +}); diff --git a/tests/unit/fixtures/stream-failure-persistent-classification.fixture.ts b/tests/unit/fixtures/stream-failure-persistent-classification.fixture.ts new file mode 100644 index 0000000000..c589a4bd02 --- /dev/null +++ b/tests/unit/fixtures/stream-failure-persistent-classification.fixture.ts @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-failure-code-")); +const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); +const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; + +fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; + +const core = await import("../../../src/lib/db/core.ts"); +const failureUsage = await import("../../../open-sse/handlers/chatCore/failureUsage.ts"); +const usageHistory = await import("../../../src/lib/usage/usageHistory.ts"); +const { createStreamFailureFinalizers } = + await import("../../../open-sse/utils/streamFailureFinalization.ts"); + +test.after(() => { + core.resetDbInstance(); + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; + else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; + fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("stream failure persists only the projected public classification", () => { + const opaqueCode = "opaque-stream-code-secret-9382746"; + let completionCode: string | null | undefined; + let persistedCode: string | undefined; + let classifierCode: string | undefined; + const { handleStreamFailure } = createStreamFailureFinalizers({ + isFailureCompletionRecorded: () => false, + onStreamComplete: (payload) => { + completionCode = payload.errorCode; + }, + persistFailureUsage: (_status, errorCode) => { + persistedCode = errorCode; + }, + onStreamFailure: (failure) => { + classifierCode = failure.code; + }, + }); + + assert.equal( + handleStreamFailure({ status: 502, message: "upstream failed", code: opaqueCode }), + true + ); + assert.equal(completionCode, "bad_gateway"); + assert.equal(persistedCode, "bad_gateway"); + assert.equal(classifierCode, opaqueCode); +}); + +test("pre-response failures persist only the projected public classification", async () => { + const opaqueCode = "opaque-pre-response-code-secret-6382951"; + const projectedCode = failureUsage.projectFailureUsageErrorCode({ + statusCode: 502, + message: "upstream request failed", + errorCode: opaqueCode, + errorType: "opaque-pre-response-type-secret-9472013", + }); + + assert.equal(projectedCode, "bad_gateway"); + + const provider = "persistent-error-code-boundary"; + await usageHistory.saveRequestUsage( + failureUsage.buildFailureUsageRecord({ + provider, + model: "model", + connectionId: null, + apiKeyInfo: null, + effectiveServiceTier: "standard", + isCombo: false, + comboStrategy: null, + statusCode: 502, + errorCode: projectedCode, + latencyMs: 1, + }) + ); + + const rows = await usageHistory.getUsageHistory({ provider }); + assert.equal(rows.length, 1); + assert.equal(rows[0]?.errorCode, "bad_gateway"); + assert.doesNotMatch(JSON.stringify(rows), /opaque-pre-response|6382951|9472013/); +}); diff --git a/tests/unit/gemini-responses-error-redaction.test.ts b/tests/unit/gemini-responses-error-redaction.test.ts index d7ff304200..f8814b3e8c 100644 --- a/tests/unit/gemini-responses-error-redaction.test.ts +++ b/tests/unit/gemini-responses-error-redaction.test.ts @@ -1,29 +1,7 @@ import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-gemini-errors-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { translateResponse, initState } = await import("../../open-sse/translator/index.ts"); -const { FORMATS } = await import("../../open-sse/translator/formats.ts"); - -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true }); -}); +import { translateResponse, initState } from "../../open-sse/translator/index.ts"; +import { FORMATS } from "../../open-sse/translator/formats.ts"; test("Gemini keeps raw failure wording internal but projects response.completed.error", () => { const state = initState(FORMATS.OPENAI_RESPONSES); diff --git a/tests/unit/helpers/runIsolatedBoundaryFixture.ts b/tests/unit/helpers/runIsolatedBoundaryFixture.ts new file mode 100644 index 0000000000..07cc672e28 --- /dev/null +++ b/tests/unit/helpers/runIsolatedBoundaryFixture.ts @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); +const CHILD_PATH = "/usr/local/bin:/usr/bin:/bin"; +const CHILD_MAX_BUFFER_BYTES = 10 * 1024 * 1024; + +type IsolatedBoundaryFixtureOptions = { + fixtureUrl: URL; + expectedTests: number; + label: string; + timeoutMs?: number; +}; + +export function runIsolatedBoundaryFixture({ + fixtureUrl, + expectedTests, + label, + timeoutMs = 180_000, +}: IsolatedBoundaryFixtureOptions): void { + const root = mkdtempSync(join(tmpdir(), "omniroute-public-error-child-")); + const dataDir = join(root, "data"); + const pluginsDir = join(root, "plugins"); + mkdirSync(dataDir, { recursive: true }); + mkdirSync(pluginsDir, { recursive: true }); + + try { + const result = spawnSync( + process.execPath, + ["--import", "tsx/esm", "--test", "--test-reporter=tap", fileURLToPath(fixtureUrl)], + { + cwd: REPO_ROOT, + encoding: "utf8", + env: { + APP_LOG_TO_FILE: "false", + API_KEY_SECRET: "public-error-boundary-fixture-secret", + DATA_DIR: dataDir, + DISABLE_SQLITE_AUTO_BACKUP: "true", + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8", + NODE_ENV: "test", + OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK: "true", + OMNIROUTE_PLUGINS_DIR: pluginsDir, + PATH: CHILD_PATH, + TZ: "UTC", + }, + maxBuffer: CHILD_MAX_BUFFER_BYTES, + timeout: timeoutMs, + } + ); + const diagnostics = [ + `${label} child status=${String(result.status)} signal=${String(result.signal)}`, + result.error ? `error=${String(result.error)}` : "", + `stdout:\n${result.stdout}`, + `stderr:\n${result.stderr}`, + ] + .filter(Boolean) + .join("\n"); + + assert.equal(result.error, undefined, diagnostics); + assert.equal(result.signal, null, diagnostics); + assert.equal(result.status, 0, diagnostics); + assert.match(result.stdout, new RegExp(`# tests ${expectedTests}(?:\\r?\\n|$)`), diagnostics); + assert.match(result.stdout, new RegExp(`# pass ${expectedTests}(?:\\r?\\n|$)`), diagnostics); + assert.match(result.stdout, /# fail 0(?:\r?\n|$)/, diagnostics); + } finally { + rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } +} diff --git a/tests/unit/mcp-public-error-boundaries.test.ts b/tests/unit/mcp-public-error-boundaries.test.ts index 594ca47bf5..92095c8f85 100644 --- a/tests/unit/mcp-public-error-boundaries.test.ts +++ b/tests/unit/mcp-public-error-boundaries.test.ts @@ -1,184 +1,11 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; -import { fileURLToPath } from "node:url"; -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mcp-error-boundaries-")); -const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -const originalApiKey = process.env.OMNIROUTE_API_KEY; -const originalApiKeyId = process.env.OMNIROUTE_API_KEY_ID; -const originalInternalToken = process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN; -const originalInternalTokenFile = process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE; -const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -process.env.OMNIROUTE_API_KEY = "mcp-boundary-test-key"; -process.env.OMNIROUTE_API_KEY_ID = "mcp-boundary-test-key-id"; -process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN = "mcp-boundary-internal-test-token"; -process.env.OMNIROUTE_BASE_URL = "http://localhost:20128"; -delete process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE; -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); +import { runIsolatedBoundaryFixture } from "./helpers/runIsolatedBoundaryFixture.ts"; -const { createMcpServer } = await import("../../open-sse/mcp-server/server.ts"); -const { closeAuditDb, queryAuditEntries } = await import("../../open-sse/mcp-server/audit.ts"); -const { obsidianTools } = await import("../../open-sse/mcp-server/tools/obsidianTools.ts"); -const { skillTools } = await import("../../open-sse/mcp-server/tools/skillTools.ts"); -const { skillRegistry } = await import("../../src/lib/skills/registry.ts"); -const { skillExecutor } = await import("../../src/lib/skills/executor.ts"); -const core = await import("../../src/lib/db/core.ts"); - -type McpResult = { - content?: Array<{ type: string; text: string }>; - isError?: boolean; -}; - -type RegisteredTool = { - handler: (args: unknown, extra?: unknown) => Promise; -}; - -function getRegisteredHandler(server: unknown, toolName: string): RegisteredTool["handler"] { - const registry = (server as { _registeredTools?: Record }) - ._registeredTools; - assert.ok(registry, "McpServer should expose _registeredTools"); - const tool = registry[toolName]; - assert.ok(tool, `${toolName} must be registered`); - return tool.handler; -} - -function assertPublicMcpError(result: McpResult): void { - const text = result.content?.[0]?.text ?? ""; - assert.equal(result.isError, true); - assert.match(text, /Error:/); - assert.doesNotMatch(text, /mcp-boundary-secret|srv\/private|mcp-boundary\.ts|\bat execute\b/i); -} - -test.after(() => { - closeAuditDb(); - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - if (originalApiKey === undefined) delete process.env.OMNIROUTE_API_KEY; - else process.env.OMNIROUTE_API_KEY = originalApiKey; - if (originalApiKeyId === undefined) delete process.env.OMNIROUTE_API_KEY_ID; - else process.env.OMNIROUTE_API_KEY_ID = originalApiKeyId; - if (originalInternalToken === undefined) delete process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN; - else process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN = originalInternalToken; - if (originalInternalTokenFile === undefined) { - delete process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE; - } else { - process.env.OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE = originalInternalTokenFile; - } - if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; - else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - -test("core MCP handlers sanitize upstream bodies before public and audit boundaries", async () => { - const hostile = "Bearer mcp-fetch-boundary-secret at /srv/private/mcp-fetch-boundary.ts:9:3"; - const originalFetch = globalThis.fetch; - const calledUrls: string[] = []; - globalThis.fetch = async (input) => { - calledUrls.push(String(input)); - return new Response(hostile, { status: 500 }); - }; - - try { - const handler = getRegisteredHandler(createMcpServer(), "omniroute_list_combos"); - const result = await handler({ includeMetrics: false }); - const publicText = result.content?.[0]?.text ?? ""; - assert.equal(result.isError, true); - assert.doesNotMatch( - publicText, - /mcp-fetch-boundary-secret|srv\/private|mcp-fetch-boundary\.ts/i - ); - assert.deepEqual(calledUrls, ["http://localhost:20128/api/combos"]); - - const audit = await queryAuditEntries({ tool: "omniroute_list_combos", success: false }); - assert.ok(audit.entries.length >= 1); - assert.doesNotMatch( - JSON.stringify(audit.entries), - /mcp-fetch-boundary-secret|srv\/private|mcp-fetch-boundary\.ts/i - ); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("every MCP public catch uses the canonical fail-closed projector", () => { - const source = fs.readFileSync(path.join(repoRoot, "open-sse/mcp-server/server.ts"), "utf8"); - assert.doesNotMatch(source, /err instanceof Error \? err\.message : String\(err\)/); -}); - -test("Obsidian and dynamic-skill MCP wrappers sanitize thrown errors", async () => { - const hostile = new Error( - "MCP failed access_token=mcp-boundary-secret at /srv/private/mcp-boundary.ts\n" + - " at execute (/srv/private/mcp-boundary.ts:9:3)" - ); - const mutableObsidianTool = obsidianTools[0] as unknown as { - name: string; - handler: (args: unknown, extra?: unknown) => Promise; - }; - const originalObsidianHandler = mutableObsidianTool.handler; - try { - mutableObsidianTool.handler = async () => { - throw hostile; - }; - const obsidianHandler = getRegisteredHandler(createMcpServer(), mutableObsidianTool.name); - assertPublicMcpError(await obsidianHandler({}, { authInfo: { scopes: ["read:obsidian"] } })); - } finally { - mutableObsidianTool.handler = originalObsidianHandler; - } - - const mutableRegistry = skillRegistry as unknown as { - list: () => Array<{ name: string; description: string; enabled: boolean }>; - }; - const mutableExecutor = skillExecutor as unknown as { - execute: (...args: unknown[]) => Promise; - }; - const originalList = mutableRegistry.list; - const originalExecute = mutableExecutor.execute; - try { - mutableRegistry.list = () => [ - { name: "mcp_boundary_skill", description: "boundary test", enabled: true }, - ]; - const dynamicHandler = getRegisteredHandler(createMcpServer(), "skill_mcp_boundary_skill"); - mutableExecutor.execute = async () => { - throw hostile; - }; - assertPublicMcpError( - await dynamicHandler({}, { authInfo: { clientId: "test", scopes: ["execute:skills"] } }) - ); - } finally { - mutableRegistry.list = originalList; - mutableExecutor.execute = originalExecute; - } -}); - -test("skill-tool MCP wrapper uses its own fail-closed fallback for hostile thrown values", async () => { - const mutableSkillTool = Object.values(skillTools)[0] as unknown as { - name: string; - handler: (args: unknown, extra?: unknown) => Promise; - }; - const originalHandler = mutableSkillTool.handler; - const revocable = Proxy.revocable({}, {}); - revocable.revoke(); - - try { - mutableSkillTool.handler = async () => { - throw revocable.proxy; - }; - const handler = getRegisteredHandler(createMcpServer(), mutableSkillTool.name); - const result = await handler({}, { authInfo: { scopes: ["read:skills"] } }); - assert.equal(result.isError, true); - assert.equal(result.content?.[0]?.text, "Error: Skill tool execution failed"); - } finally { - mutableSkillTool.handler = originalHandler; - } +test("MCP public error boundaries pass in an isolated child process", () => { + runIsolatedBoundaryFixture({ + fixtureUrl: new URL("./fixtures/mcp-public-error-boundaries.fixture.ts", import.meta.url), + expectedTests: 4, + label: "MCP public error boundaries", + }); }); diff --git a/tests/unit/moderations-handler.test.ts b/tests/unit/moderations-handler.test.ts index b7d491a529..640d0a68bc 100644 --- a/tests/unit/moderations-handler.test.ts +++ b/tests/unit/moderations-handler.test.ts @@ -1,18 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-moderations-handler-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); const { handleModeration } = await import("../../open-sse/handlers/moderations.ts"); const { MODERATION_PROVIDERS, getModerationProvider, parseModerationModel } = await import("../../open-sse/config/moderationRegistry.ts"); @@ -23,15 +11,6 @@ test.afterEach(() => { globalThis.fetch = originalFetch; }); -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - test("MODERATION_PROVIDERS registers mistral with the Mistral moderations base URL", () => { const provider = getModerationProvider("mistral"); assert.ok(provider); diff --git a/tests/unit/ocr-handler-dispatch.test.ts b/tests/unit/ocr-handler-dispatch.test.ts index 6724150d19..df8495b095 100644 --- a/tests/unit/ocr-handler-dispatch.test.ts +++ b/tests/unit/ocr-handler-dispatch.test.ts @@ -1,28 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ocr-handler-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { handleOcr } = await import("../../open-sse/handlers/ocr.ts"); - -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); +import { handleOcr } from "../../open-sse/handlers/ocr.ts"; function fetchStub( script: Array<{ status: number; headers?: Record; json?: unknown }> diff --git a/tests/unit/provider-connection-test-error-boundaries.test.ts b/tests/unit/provider-connection-test-error-boundaries.test.ts index 4cce94a981..4db74062b0 100644 --- a/tests/unit/provider-connection-test-error-boundaries.test.ts +++ b/tests/unit/provider-connection-test-error-boundaries.test.ts @@ -1,262 +1,14 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-provider-errors-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -const originalApiKeySecret = process.env.API_KEY_SECRET; -const originalDisableBackup = process.env.DISABLE_SQLITE_AUTO_BACKUP; -const originalDisableHealthCheck = process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK; -const pluginsDir = path.join(testRoot, "plugins"); -const testDataDir = path.join(testRoot, "data"); -fs.mkdirSync(pluginsDir, { recursive: true }); -fs.mkdirSync(testDataDir, { recursive: true }); -process.env.OMNIROUTE_PLUGINS_DIR = pluginsDir; -process.env.DATA_DIR = testDataDir; -assert.notEqual(fs.realpathSync(testDataDir), "/home/diegosouzapw/.omniroute"); -assert.notEqual(fs.realpathSync(pluginsDir), "/home/diegosouzapw/.omniroute/plugins"); +import { runIsolatedBoundaryFixture } from "./helpers/runIsolatedBoundaryFixture.ts"; -process.env.API_KEY_SECRET = "provider-error-boundary-test-secret"; -process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; -process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK = "true"; - -// Connection tests suppress their call-log entry under node --test. This file -// exercises the real persistent boundary, so present a normal runtime identity -// before importing the route and its logging modules. -const originalArgv = process.argv; -const originalExecArgv = process.execArgv; -const originalNodeEnv = process.env.NODE_ENV; -const originalVitest = process.env.VITEST; -process.argv = [ - process.execPath, - path.join(process.cwd(), "scripts/ad-hoc/omniroute-boundary-harness.mjs"), -]; -process.execArgv = []; -process.env.NODE_ENV = "development"; -delete process.env.VITEST; - -const hostileValidationMessage = - "Jules failed access_token=jules-boundary-secret at /srv/private/validator.ts\n" + - " at probe (/srv/private/validator.ts:42:7)"; -const julesValidationUrl = "https://jules.googleapis.com/v1alpha/sources"; -const originalFetch = globalThis.fetch; -let validationFetchCalls = 0; -const boundaryFetch = (async (input: string | URL | Request) => { - const url = - typeof input === "string" ? input : input instanceof Request ? input.url : input.toString(); - assert.equal(url, julesValidationUrl, `unexpected outbound request: ${url}`); - validationFetchCalls += 1; - return new Response(hostileValidationMessage, { status: 500 }); -}) as typeof fetch; -globalThis.fetch = boundaryFetch; - -const core = await import("../../src/lib/db/core.ts"); -const providersDb = await import("../../src/lib/db/providers.ts"); -const { saveCallLog, waitForCallLogSaves, closeCallLogSaves } = - await import("../../src/lib/usage/callLogs.ts"); -const { flushProxyLogsSync } = await import("../../src/lib/proxyLogger.ts"); -const { projectProviderRuntimeForPublicResponse, testSingleConnection } = - await import("../../src/app/api/providers/[id]/test/route.ts"); -// proxyFetch installs its global dispatcher while the imports above load. Put -// the deterministic stub back at the final fetch seam so this test can never -// reach Jules over the network. -globalThis.fetch = boundaryFetch; - -type ArtifactRow = { artifact_relpath: string | null; error_summary: string | null }; - -function isRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -function readArtifact(relativePath: string | null): Record { - assert.ok(relativePath, "call log must have a persisted detail artifact"); - const absolutePath = path.join(testDataDir, "call_logs", relativePath); - return JSON.parse(fs.readFileSync(absolutePath, "utf8")) as Record; -} - -test.after(async () => { - await closeCallLogSaves(2_000); - flushProxyLogsSync(); - globalThis.fetch = originalFetch; - process.argv = originalArgv; - process.execArgv = originalExecArgv; - if (originalNodeEnv === undefined) delete process.env.NODE_ENV; - else process.env.NODE_ENV = originalNodeEnv; - if (originalVitest === undefined) delete process.env.VITEST; - else process.env.VITEST = originalVitest; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalApiKeySecret === undefined) delete process.env.API_KEY_SECRET; - else process.env.API_KEY_SECRET = originalApiKeySecret; - if (originalDisableBackup === undefined) delete process.env.DISABLE_SQLITE_AUTO_BACKUP; - else process.env.DISABLE_SQLITE_AUTO_BACKUP = originalDisableBackup; - if (originalDisableHealthCheck === undefined) { - delete process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK; - } else { - process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK = originalDisableHealthCheck; - } - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - -test("public runtime projection omits host paths and internal error envelopes", () => { - const projected = projectProviderRuntimeForPublicResponse({ - installed: true, - runnable: false, - requiresBinary: true, - reason: "not_executable", - runtimeMode: "local", - version: "v1 from /srv/private/bin/tool", - command: "/srv/private/bin/tool", - commandPath: "/srv/private/bin/tool", - settingsPath: "C:\\Users\\admin\\.config\\tool.json", - error: "access_token=runtime-secret at /srv/private/runtime.json", - diagnosis: { message: "runtime-secret at /srv/private/runtime.ts" }, +test("provider connection error boundaries pass in an isolated child process", () => { + runIsolatedBoundaryFixture({ + fixtureUrl: new URL( + "./fixtures/provider-connection-test-error-boundaries.fixture.ts", + import.meta.url + ), + expectedTests: 3, + label: "provider connection error boundaries", }); - const serialized = JSON.stringify(projected); - - assert.equal(projected?.installed, true); - assert.equal(projected?.runnable, false); - assert.equal("commandPath" in (projected || {}), false); - assert.equal("settingsPath" in (projected || {}), false); - assert.equal("error" in (projected || {}), false); - assert.equal("diagnosis" in (projected || {}), false); - assert.doesNotMatch(serialized, /runtime-secret|srv\/private|C:\\\\Users/i); -}); - -test("connection validation projects hostile errors before public and persistent boundaries", async () => { - const connection = await providersDb.createProviderConnection({ - provider: "jules", - authType: "apikey", - name: "Jules Error Boundary", - apiKey: "jules-test-key", - isActive: true, - testStatus: "active", - }); - assert.ok(connection?.id); - - const result = await testSingleConnection(connection.id); - assert.equal(result.valid, false); - assert.ok(validationFetchCalls > 0, "the deterministic Jules stub must handle the probe"); - assert.match(String(result.error), /Jules failed/i); - assert.equal(await waitForCallLogSaves(10_000), true, "call-log write must drain"); - flushProxyLogsSync(); - - const db = core.getDbInstance(); - const providerRow = db - .prepare("SELECT last_error FROM provider_connections WHERE id = ?") - .get(connection.id) as { last_error: string | null }; - const callLogRow = db - .prepare( - `SELECT error_summary, artifact_relpath - FROM call_logs - WHERE connection_id = ? AND model = 'connection-test' - ORDER BY rowid DESC LIMIT 1` - ) - .get(connection.id) as ArtifactRow; - const proxyLogRow = db - .prepare( - `SELECT error - FROM proxy_logs - WHERE connection_id = ? AND provider = 'jules' - AND target_url = 'jules/connection-test' - ORDER BY rowid DESC LIMIT 1` - ) - .get(connection.id) as { error: string | null }; - assert.ok(callLogRow, "connection test must write call_logs"); - assert.ok(proxyLogRow, "connection test must write proxy_logs"); - const artifact = readArtifact(callLogRow.artifact_relpath); - - const boundaries = { - publicResult: result, - providerLastError: providerRow.last_error, - callLogSummary: callLogRow.error_summary, - callLogArtifactError: artifact.error, - proxyLogError: proxyLogRow.error, - }; - const leakPattern = /jules-boundary-secret|srv\/private|validator\.ts|\bat probe\b/i; - const leakingBoundaries = Object.entries(boundaries) - .filter(([, value]) => leakPattern.test(JSON.stringify(value))) - .map(([name]) => name); - assert.deepEqual(leakingBoundaries, []); -}); - -test("failed call logs sanitize response-body copies while successful bodies stay unchanged", async () => { - const hostileBody = { - message: "access_token=call-body-secret at /srv/private/upstream.json", - detail: "Error: api_key=call-detail-secret\n at dispatch (/srv/private/rerank.ts:7:2)", - }; - const successBody = { - message: "Successful output mentions /tmp/public-example.ts and remains unchanged", - usage: { total_tokens: 4 }, - }; - - await saveCallLog({ - id: "error-body-json", - status: 502, - provider: "rerank-test", - model: "rerank-test", - responseBody: hostileBody, - pipelinePayloads: { - providerResponse: { body: hostileBody }, - clientResponse: { body: hostileBody }, - }, - }); - await saveCallLog({ - id: "error-body-text", - status: 503, - provider: "rerank-test", - model: "rerank-test", - responseBody: "Bearer plaintext-body-secret at C:\\Users\\admin\\upstream.txt", - }); - await saveCallLog({ - id: "success-body-control", - status: 200, - provider: "rerank-test", - model: "rerank-test", - responseBody: successBody, - pipelinePayloads: { - providerResponse: { body: successBody }, - clientResponse: { body: successBody }, - }, - }); - await saveCallLog({ - id: "error-body-binary", - status: 500, - provider: "rerank-test", - model: "rerank-test", - responseBody: Buffer.from([1, 2, 3, 4]), - }); - assert.equal(await waitForCallLogSaves(2_000), true, "call-log writes must drain"); - - const db = core.getDbInstance(); - const rows = db - .prepare( - `SELECT id, artifact_relpath FROM call_logs - WHERE id IN ( - 'error-body-json', 'error-body-text', 'success-body-control', 'error-body-binary' - )` - ) - .all() as Array<{ id: string; artifact_relpath: string | null }>; - const artifacts = Object.fromEntries( - rows.map((row) => [row.id, readArtifact(row.artifact_relpath)]) - ) as Record>; - - assert.doesNotMatch( - JSON.stringify({ json: artifacts["error-body-json"], text: artifacts["error-body-text"] }), - /call-body-secret|call-detail-secret|plaintext-body-secret|srv\/private|C:\\\\Users|\bat dispatch\b/i - ); - assert.deepEqual(artifacts["success-body-control"].responseBody, successBody); - assert.equal(artifacts["error-body-binary"].responseBody, "[binary 4 bytes]"); - const pipeline = artifacts["success-body-control"].pipeline; - assert.ok(isRecord(pipeline)); - assert.ok(isRecord(pipeline.providerResponse)); - assert.ok(isRecord(pipeline.clientResponse)); - assert.deepEqual(pipeline.providerResponse.body, successBody); - assert.deepEqual(pipeline.clientResponse.body, successBody); }); diff --git a/tests/unit/provider-last-error-sanitization.test.ts b/tests/unit/provider-last-error-sanitization.test.ts index 67ecbf61d3..e7dee5071a 100644 --- a/tests/unit/provider-last-error-sanitization.test.ts +++ b/tests/unit/provider-last-error-sanitization.test.ts @@ -1,109 +1,11 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-provider-last-error-")); -const testDataDir = path.join(testRoot, "data"); -const testPluginsDir = path.join(testRoot, "plugins"); -const originalEnv = { - DATA_DIR: process.env.DATA_DIR, - OMNIROUTE_PLUGINS_DIR: process.env.OMNIROUTE_PLUGINS_DIR, - API_KEY_SECRET: process.env.API_KEY_SECRET, - DISABLE_SQLITE_AUTO_BACKUP: process.env.DISABLE_SQLITE_AUTO_BACKUP, -}; -fs.mkdirSync(testDataDir, { recursive: true }); -fs.mkdirSync(testPluginsDir, { recursive: true }); -process.env.DATA_DIR = testDataDir; -process.env.OMNIROUTE_PLUGINS_DIR = testPluginsDir; -process.env.API_KEY_SECRET = "provider-last-error-test-secret"; -process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +import { runIsolatedBoundaryFixture } from "./helpers/runIsolatedBoundaryFixture.ts"; -const core = await import("../../src/lib/db/core.ts"); -const providersDb = await import("../../src/lib/db/providers.ts"); -const loggerResource = await import("../../src/shared/utils/loggerResource.ts"); -const { runAsProbe } = await import("../../src/shared/utils/probeOrigin.ts"); -const { writeTerminalStatus } = await import("../../src/shared/utils/terminalStatus.ts"); -const { markAccountUnavailable } = await import("../../src/sse/services/auth.ts"); - -function restoreEnv(name: keyof typeof originalEnv): void { - const original = originalEnv[name]; - if (original === undefined) delete process.env[name]; - else process.env[name] = original; -} - -function readLastError(connectionId: string): string | null { - const row = core - .getDbInstance() - .prepare("SELECT last_error FROM provider_connections WHERE id = ?") - .get(connectionId) as { last_error: string | null } | undefined; - return row?.last_error ?? null; -} - -test.after(async () => { - core.resetDbInstance(); - await loggerResource.closeSharedLoggerResource(); - restoreEnv("DATA_DIR"); - restoreEnv("OMNIROUTE_PLUGINS_DIR"); - restoreEnv("API_KEY_SECRET"); - restoreEnv("DISABLE_SQLITE_AUTO_BACKUP"); - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - -test("normal and probe failures sanitize provider_connections.lastError at the write seam", async () => { - const hostile = - "provider failed access_token=provider-last-error-secret at /srv/private/provider.ts\n" + - " at dispatch (/srv/private/provider.ts:12:4)"; - const normal = await providersDb.createProviderConnection({ - provider: "openai", - authType: "apikey", - name: "normal last-error boundary", - apiKey: "normal-last-error-test-key", // pragma: allowlist secret - isActive: true, - testStatus: "active", +test("provider last-error persistence passes in an isolated child process", () => { + runIsolatedBoundaryFixture({ + fixtureUrl: new URL("./fixtures/provider-last-error-sanitization.fixture.ts", import.meta.url), + expectedTests: 1, + label: "provider last-error persistence", }); - const probe = await providersDb.createProviderConnection({ - provider: "openai", - authType: "apikey", - name: "probe last-error boundary", - apiKey: "probe-last-error-test-key", // pragma: allowlist secret - isActive: true, - testStatus: "active", - }); - const terminal = await providersDb.createProviderConnection({ - provider: "openai", - authType: "apikey", - name: "terminal last-error boundary", - apiKey: "terminal-last-error-test-key", // pragma: allowlist secret - isActive: true, - testStatus: "active", - }); - - await markAccountUnavailable(normal.id, 500, hostile, "openai"); - await runAsProbe(() => markAccountUnavailable(probe.id, 500, hostile, "openai")); - await writeTerminalStatus( - terminal.id, - { - testStatus: "banned", - isActive: false, - lastError: hostile, - lastErrorType: "forbidden", - errorCode: "403", - }, - "production" - ); - - const persisted = { - normal: readLastError(normal.id), - probe: readLastError(probe.id), - terminal: readLastError(terminal.id), - }; - assert.match(String(persisted.normal), /provider failed/i); - assert.match(String(persisted.probe), /provider failed/i); - assert.match(String(persisted.terminal), /provider failed/i); - assert.doesNotMatch( - JSON.stringify(persisted), - /provider-last-error-secret|srv\/private|provider\.ts|\bat dispatch\b/i - ); }); diff --git a/tests/unit/provider-validation-error-sanitization.test.ts b/tests/unit/provider-validation-error-sanitization.test.ts index 2d62ce5445..4a725216b9 100644 --- a/tests/unit/provider-validation-error-sanitization.test.ts +++ b/tests/unit/provider-validation-error-sanitization.test.ts @@ -1,32 +1,10 @@ import assert from "node:assert/strict"; import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; - -const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-provider-validation-errors-")); -const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); -const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); -const ORIGINAL_DATA_DIR = process.env.DATA_DIR; -const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; - -fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); -fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); -process.env.DATA_DIR = TEST_DATA_DIR; -process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; - -const core = await import("../../src/lib/db/core.ts"); -const { projectProviderValidationResultForPublicResponse, toValidationErrorResult } = - await import("../../src/lib/providers/validation/transport.ts"); - -test.after(() => { - core.resetDbInstance(); - if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = ORIGINAL_DATA_DIR; - if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; - fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); +import { + projectProviderValidationResultForPublicResponse, + toValidationErrorResult, +} from "../../src/lib/providers/validation/transport.ts"; test("provider validation sanitizes thrown error details", () => { const result = toValidationErrorResult( diff --git a/tests/unit/request-log-management-boundary.test.ts b/tests/unit/request-log-management-boundary.test.ts index bc80d345f4..2b619774db 100644 --- a/tests/unit/request-log-management-boundary.test.ts +++ b/tests/unit/request-log-management-boundary.test.ts @@ -1,108 +1,11 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; -const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-log-management-boundary-")); -const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); -const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); -const ORIGINAL_DATA_DIR = process.env.DATA_DIR; -const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; -const ORIGINAL_DISABLE_BACKUP = process.env.DISABLE_SQLITE_AUTO_BACKUP; +import { runIsolatedBoundaryFixture } from "./helpers/runIsolatedBoundaryFixture.ts"; -fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); -fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); -process.env.DATA_DIR = TEST_DATA_DIR; -process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; -process.env.DISABLE_SQLITE_AUTO_BACKUP = "1"; - -const core = await import("../../src/lib/db/core.ts"); -const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); -const logsRoute = await import("../../src/app/api/logs/[id]/route.ts"); -const usageHistoryRoute = await import("../../src/app/api/usage/history/route.ts"); - -test.afterEach(() => { - usageHistory.clearPendingRequests(); -}); - -test.after(() => { - usageHistory.clearPendingRequests(); - core.resetDbInstance(); - if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = ORIGINAL_DATA_DIR; - if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; - if (ORIGINAL_DISABLE_BACKUP === undefined) delete process.env.DISABLE_SQLITE_AUTO_BACKUP; - else process.env.DISABLE_SQLITE_AUTO_BACKUP = ORIGINAL_DISABLE_BACKUP; - fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - -const HOSTILE = - "Bearer management-cache-secret at /srv/private/completed-request.ts:12:3\n" + - " at finalize (/srv/private/finalize.ts:4:2)"; - -async function readManagementDetail(id: string): Promise> { - const response = await logsRoute.GET(undefined as unknown as Request, { params: { id } }); - assert.equal(response.status, 200); - return (await response.json()) as Record; -} - -function serializedDetail(detail: Record): string { - return JSON.stringify(detail); -} - -test("management detail sanitizes in-flight failure chunks at the endpoint boundary", async () => { - const requestId = usageHistory.trackPendingRequest("model", "provider", "conn-inflight", true); - assert.ok(requestId); - usageHistory.updatePendingRequestStreamChunks("model", "provider", "conn-inflight", { - provider: [`event: error\ndata: ${HOSTILE}\n\n`], - openai: [], - client: [], +test("request-log management boundaries pass in an isolated child process", () => { + runIsolatedBoundaryFixture({ + fixtureUrl: new URL("./fixtures/request-log-management-boundary.fixture.ts", import.meta.url), + expectedTests: 3, + label: "request-log management boundaries", }); - - const detail = await readManagementDetail(requestId); - assert.doesNotMatch( - serializedDetail(detail), - /management-cache-secret|srv\/private|completed-request\.ts|\bat finalize\b/i - ); -}); - -test("management detail sanitizes completed error metadata and cached chunks", async () => { - const requestId = usageHistory.trackPendingRequest("model", "provider", "conn-completed", true); - assert.ok(requestId); - usageHistory.updatePendingRequestStreamChunks("model", "provider", "conn-completed", { - provider: [`data: ${JSON.stringify({ type: "error", message: HOSTILE })}\n\n`], - openai: [], - client: [], - }); - assert.equal( - usageHistory.finalizePendingRequestById(requestId, { status: 502, error: HOSTILE }), - true - ); - - const detail = await readManagementDetail(requestId); - assert.doesNotMatch( - serializedDetail(detail), - /management-cache-secret|srv\/private|completed-request\.ts|\bat finalize\b/i - ); -}); - -test("usage history endpoint exposes pending counters without raw request details", async () => { - const requestId = usageHistory.trackPendingRequest("model", "provider", "conn-usage", true); - assert.ok(requestId); - usageHistory.updatePendingRequestStreamChunks("model", "provider", "conn-usage", { - provider: [`event: error\ndata: ${HOSTILE}\n\n`], - openai: [], - client: [], - }); - - const response = await usageHistoryRoute.GET(undefined as unknown as Request); - assert.equal(response.status, 200); - const body = (await response.json()) as { - pending?: { byModel?: Record; details?: unknown }; - }; - assert.equal(body.pending?.byModel?.["model (provider)"], 1); - assert.equal("details" in (body.pending ?? {}), false); - assert.doesNotMatch(JSON.stringify(body), /management-cache-secret|srv\/private/i); }); diff --git a/tests/unit/request-log-payloads.test.ts b/tests/unit/request-log-payloads.test.ts index dcc766f777..4d5557a6fb 100644 --- a/tests/unit/request-log-payloads.test.ts +++ b/tests/unit/request-log-payloads.test.ts @@ -1,19 +1,6 @@ +import { protectPipelinePayloads } from "../../src/lib/usage/callLogs/format.ts"; import test from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-request-log-payloads-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { protectPipelinePayloads } = await import("../../src/lib/usage/callLogs/format.ts"); const { normalizePayloadForLog, @@ -29,15 +16,6 @@ const { } = await import("../../open-sse/utils/streamPayloadCollector.ts"); const { FORMATS } = await import("../../open-sse/translator/formats.ts"); -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - test("normalizes JSON strings before log protection and redacts sensitive keys", () => { const protectedPayload = protectPayloadForLog( JSON.stringify({ diff --git a/tests/unit/stream-failure-persistent-classification.test.ts b/tests/unit/stream-failure-persistent-classification.test.ts index a7260d5258..481cf8cc6f 100644 --- a/tests/unit/stream-failure-persistent-classification.test.ts +++ b/tests/unit/stream-failure-persistent-classification.test.ts @@ -1,91 +1,14 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; -const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-failure-code-")); -const TEST_DATA_DIR = path.join(TEST_ROOT, "data"); -const TEST_PLUGINS_DIR = path.join(TEST_ROOT, "plugins"); -const ORIGINAL_DATA_DIR = process.env.DATA_DIR; -const ORIGINAL_PLUGINS_DIR = process.env.OMNIROUTE_PLUGINS_DIR; +import { runIsolatedBoundaryFixture } from "./helpers/runIsolatedBoundaryFixture.ts"; -fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); -fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true }); -process.env.DATA_DIR = TEST_DATA_DIR; -process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; - -const core = await import("../../src/lib/db/core.ts"); -const failureUsage = await import("../../open-sse/handlers/chatCore/failureUsage.ts"); -const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); -const { createStreamFailureFinalizers } = - await import("../../open-sse/utils/streamFailureFinalization.ts"); - -test.after(() => { - core.resetDbInstance(); - if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = ORIGINAL_DATA_DIR; - if (ORIGINAL_PLUGINS_DIR === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = ORIGINAL_PLUGINS_DIR; - fs.rmSync(TEST_ROOT, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); - -test("stream failure persists only the projected public classification", () => { - const opaqueCode = "opaque-stream-code-secret-9382746"; - let completionCode: string | null | undefined; - let persistedCode: string | undefined; - let classifierCode: string | undefined; - const { handleStreamFailure } = createStreamFailureFinalizers({ - isFailureCompletionRecorded: () => false, - onStreamComplete: (payload) => { - completionCode = payload.errorCode; - }, - persistFailureUsage: (_status, errorCode) => { - persistedCode = errorCode; - }, - onStreamFailure: (failure) => { - classifierCode = failure.code; - }, +test("stream failure persistence boundaries pass in an isolated child process", () => { + runIsolatedBoundaryFixture({ + fixtureUrl: new URL( + "./fixtures/stream-failure-persistent-classification.fixture.ts", + import.meta.url + ), + expectedTests: 2, + label: "stream failure persistence boundaries", }); - - assert.equal( - handleStreamFailure({ status: 502, message: "upstream failed", code: opaqueCode }), - true - ); - assert.equal(completionCode, "bad_gateway"); - assert.equal(persistedCode, "bad_gateway"); - assert.equal(classifierCode, opaqueCode); -}); - -test("pre-response failures persist only the projected public classification", async () => { - const opaqueCode = "opaque-pre-response-code-secret-6382951"; - const projectedCode = failureUsage.projectFailureUsageErrorCode({ - statusCode: 502, - message: "upstream request failed", - errorCode: opaqueCode, - errorType: "opaque-pre-response-type-secret-9472013", - }); - - assert.equal(projectedCode, "bad_gateway"); - - const provider = "persistent-error-code-boundary"; - await usageHistory.saveRequestUsage( - failureUsage.buildFailureUsageRecord({ - provider, - model: "model", - connectionId: null, - apiKeyInfo: null, - effectiveServiceTier: "standard", - isCombo: false, - comboStrategy: null, - statusCode: 502, - errorCode: projectedCode, - latencyMs: 1, - }) - ); - - const rows = await usageHistory.getUsageHistory({ provider }); - assert.equal(rows.length, 1); - assert.equal(rows[0]?.errorCode, "bad_gateway"); - assert.doesNotMatch(JSON.stringify(rows), /opaque-pre-response|6382951|9472013/); }); diff --git a/tests/unit/stream-passthrough-error-redaction.test.ts b/tests/unit/stream-passthrough-error-redaction.test.ts index 9f0beebb2c..f8efb988c2 100644 --- a/tests/unit/stream-passthrough-error-redaction.test.ts +++ b/tests/unit/stream-passthrough-error-redaction.test.ts @@ -1,29 +1,7 @@ import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-errors-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); - -const core = await import("../../src/lib/db/core.ts"); -const { createSSEStream } = await import("../../open-sse/utils/stream.ts"); -const { FORMATS } = await import("../../open-sse/translator/formats.ts"); - -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true }); -}); +import { createSSEStream } from "../../open-sse/utils/stream.ts"; +import { FORMATS } from "../../open-sse/translator/formats.ts"; type Failure = { status: number; message: string; code?: string; type?: string }; diff --git a/tests/unit/upstream-error-passthrough.test.ts b/tests/unit/upstream-error-passthrough.test.ts index 08efccc55d..a89303c964 100644 --- a/tests/unit/upstream-error-passthrough.test.ts +++ b/tests/unit/upstream-error-passthrough.test.ts @@ -1,30 +1,10 @@ import test from "node:test"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-upstream-passthrough-")); -const originalDataDir = process.env.DATA_DIR; -const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR; -process.env.DATA_DIR = path.join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = path.join(testRoot, "plugins"); -fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -fs.mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); -const core = await import("../../src/lib/db/core.ts"); -const { shouldPassthroughUpstreamError, buildPassthroughErrorResponse } = - await import("../../open-sse/utils/upstreamErrorPassthrough.ts"); -const { buildSanitizedUpstreamErrorResponse } = - await import("../../open-sse/utils/upstreamErrorResponse.ts"); - -test.after(() => { - core.resetDbInstance(); - if (originalDataDir === undefined) delete process.env.DATA_DIR; - else process.env.DATA_DIR = originalDataDir; - if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR; - else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir; - fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); -}); +import { + shouldPassthroughUpstreamError, + buildPassthroughErrorResponse, +} from "../../open-sse/utils/upstreamErrorPassthrough.ts"; +import { buildSanitizedUpstreamErrorResponse } from "../../open-sse/utils/upstreamErrorResponse.ts"; test("upstream error passthrough", async (t) => { await t.test("4xx com corpo JSON de erro do provider é elegível", () => {