mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
test: isolate stateful error boundary fixtures
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <path>");
|
||||
assert.equal(quoted, 'Provider failed opening "<path>"');
|
||||
assert.equal(singleSegment, "Provider failed opening <path>");
|
||||
assert.equal(prose, String.raw`Provider reported \offline without a path`);
|
||||
for (const projected of escapedInitialPaths) {
|
||||
assert.equal(projected, "Provider failed at <path>");
|
||||
}
|
||||
});
|
||||
|
||||
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, /<path>/);
|
||||
assert.equal(route, "Route /dashboard/providers is unavailable");
|
||||
assert.equal(singleSegment, "Provider failed opening <path>");
|
||||
assert.equal(singleSegmentRoute, "Route /vault is unavailable");
|
||||
assert.equal(compoundPathAndRoute, "Failed <path> 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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<typeof buildErrorBody> | 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<string, unknown>;
|
||||
};
|
||||
|
||||
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<string, unknown> = { 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<string, unknown>;
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
608
tests/unit/fixtures/error-public-boundaries-hardening.fixture.ts
Normal file
608
tests/unit/fixtures/error-public-boundaries-hardening.fixture.ts
Normal file
@@ -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 <path>");
|
||||
assert.equal(quoted, 'Provider failed opening "<path>"');
|
||||
assert.equal(singleSegment, "Provider failed opening <path>");
|
||||
assert.equal(prose, String.raw`Provider reported \offline without a path`);
|
||||
for (const projected of escapedInitialPaths) {
|
||||
assert.equal(projected, "Provider failed at <path>");
|
||||
}
|
||||
});
|
||||
|
||||
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, /<path>/);
|
||||
assert.equal(route, "Route /dashboard/providers is unavailable");
|
||||
assert.equal(singleSegment, "Provider failed opening <path>");
|
||||
assert.equal(singleSegmentRoute, "Route /vault is unavailable");
|
||||
assert.equal(compoundPathAndRoute, "Failed <path> 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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<typeof buildErrorBody> | 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<string, unknown>;
|
||||
};
|
||||
|
||||
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<string, unknown> = { 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<string, unknown>;
|
||||
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);
|
||||
});
|
||||
184
tests/unit/fixtures/mcp-public-error-boundaries.fixture.ts
Normal file
184
tests/unit/fixtures/mcp-public-error-boundaries.fixture.ts
Normal file
@@ -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<McpResult>;
|
||||
};
|
||||
|
||||
function getRegisteredHandler(server: unknown, toolName: string): RegisteredTool["handler"] {
|
||||
const registry = (server as { _registeredTools?: Record<string, RegisteredTool> })
|
||||
._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<unknown>;
|
||||
};
|
||||
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<unknown>;
|
||||
};
|
||||
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<unknown>;
|
||||
};
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -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<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readArtifact(relativePath: string | null): Record<string, unknown> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, Record<string, unknown>>;
|
||||
|
||||
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);
|
||||
});
|
||||
109
tests/unit/fixtures/provider-last-error-sanitization.fixture.ts
Normal file
109
tests/unit/fixtures/provider-last-error-sanitization.fixture.ts
Normal file
@@ -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
|
||||
);
|
||||
});
|
||||
108
tests/unit/fixtures/request-log-management-boundary.fixture.ts
Normal file
108
tests/unit/fixtures/request-log-management-boundary.fixture.ts
Normal file
@@ -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<Record<string, unknown>> {
|
||||
const response = await logsRoute.GET(undefined as unknown as Request, { params: { id } });
|
||||
assert.equal(response.status, 200);
|
||||
return (await response.json()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function serializedDetail(detail: Record<string, unknown>): 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<string, number>; 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);
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
73
tests/unit/helpers/runIsolatedBoundaryFixture.ts
Normal file
73
tests/unit/helpers/runIsolatedBoundaryFixture.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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<McpResult>;
|
||||
};
|
||||
|
||||
function getRegisteredHandler(server: unknown, toolName: string): RegisteredTool["handler"] {
|
||||
const registry = (server as { _registeredTools?: Record<string, RegisteredTool> })
|
||||
._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<unknown>;
|
||||
};
|
||||
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<unknown>;
|
||||
};
|
||||
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<unknown>;
|
||||
};
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, string>; json?: unknown }>
|
||||
|
||||
@@ -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<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readArtifact(relativePath: string | null): Record<string, unknown> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, Record<string, unknown>>;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<Record<string, unknown>> {
|
||||
const response = await logsRoute.GET(undefined as unknown as Request, { params: { id } });
|
||||
assert.equal(response.status, 200);
|
||||
return (await response.json()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function serializedDetail(detail: Record<string, unknown>): 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<string, number>; 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);
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user