mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-03 21:42:41 +03:00
Compare commits
5 Commits
fix/v3851-
...
security/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee3f2cf691 | ||
|
|
14fea12864 | ||
|
|
0130349906 | ||
|
|
0d0aaaf0aa | ||
|
|
e545d256db |
@@ -97,10 +97,6 @@ _Living section — cycle opened at the v3.8.50 freeze (parallel-cycle model). B
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **security(streaming):** sanitize generic mid-stream error messages before emitting OpenAI,
|
||||
Responses, or Claude SSE failure frames and before diagnostic logging, while preserving raw
|
||||
failures for internal classification and keeping client disconnects out of provider failure state.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
---
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
const ADAPTA_APP_URL = "https://agent.adapta.one";
|
||||
const ADAPTA_CLERK_URL = "https://clerk.agent.adapta.one";
|
||||
const ADAPTA_STREAM_URL = `${ADAPTA_APP_URL}/api/chat/stream/v1`;
|
||||
const ADAPTA_PUBLIC_STREAM_ERROR = `\n\n[Erro: ${sanitizeErrorMessage("Adapta upstream error")}]`;
|
||||
|
||||
// Default model ID in Adapta's internal system (corresponds to "ONE" / auto-select)
|
||||
const DEFAULT_AI_MODEL_ID = 14;
|
||||
@@ -308,10 +309,9 @@ function transformStream(adaptaStream: ReadableStream, model: string): ReadableS
|
||||
if (event.id === "quick-response") continue;
|
||||
// Real text ended — stream will send more events or close
|
||||
} else if (type === "error") {
|
||||
const errText = String(event.errorText ?? "Adapta upstream error");
|
||||
ensureRole();
|
||||
// Emit the error as content so the user sees it
|
||||
chunk({ content: `\n\n[Erro: ${errText}]` });
|
||||
// Keep upstream diagnostics private: the transformed SSE is a public HTTP 200 body.
|
||||
chunk({ content: ADAPTA_PUBLIC_STREAM_ERROR });
|
||||
finalize();
|
||||
return;
|
||||
} else if (type === "done" || type === "end") {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { trackPendingRequest } from "@/lib/usageDb";
|
||||
import { STREAM_IDLE_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import { buildErrorBody } from "./error.ts";
|
||||
import { PENDING_REQUEST_CLEARED_MARKER } from "./stream.ts";
|
||||
import { createCompletedResponsesToolHandoffWatcher } from "./responsesToolHandoff.ts";
|
||||
import { createStreamContentWatcher, type StreamContentWatcher } from "./streamReadiness.ts";
|
||||
@@ -188,10 +187,6 @@ function getErrorStatusCode(error: unknown): number {
|
||||
return 502;
|
||||
}
|
||||
|
||||
function getPublicErrorMessage(errorMsg: string, statusCode: number): string {
|
||||
return buildErrorBody(statusCode, errorMsg).error.message;
|
||||
}
|
||||
|
||||
function isDeadlineAbortReason(reason: unknown): reason is Error {
|
||||
return (
|
||||
reason instanceof Error &&
|
||||
@@ -411,7 +406,7 @@ export function createStreamController({
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
logStream(`error: ${getPublicErrorMessage(error.message, getErrorStatusCode(error))}`);
|
||||
logStream(`error: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
logStream("error: unknown");
|
||||
@@ -457,7 +452,6 @@ export function buildStreamErrorChunks(
|
||||
clientResponseFormat?: string | null
|
||||
) {
|
||||
const statusMapping = getStreamErrorStatusMapping(statusCode);
|
||||
const publicErrorMessage = getPublicErrorMessage(errorMsg, statusCode);
|
||||
|
||||
if (isResponsesClientFormat(clientResponseFormat)) {
|
||||
const errorEvent = {
|
||||
@@ -466,7 +460,7 @@ export function buildStreamErrorChunks(
|
||||
id: null,
|
||||
status: "failed",
|
||||
error: {
|
||||
message: publicErrorMessage,
|
||||
message: errorMsg,
|
||||
type: statusMapping.responses.type,
|
||||
code: statusMapping.responses.code,
|
||||
},
|
||||
@@ -481,7 +475,7 @@ export function buildStreamErrorChunks(
|
||||
type: "error",
|
||||
error: {
|
||||
type: statusMapping.claude.type,
|
||||
message: publicErrorMessage,
|
||||
message: errorMsg,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -504,7 +498,7 @@ export function buildStreamErrorChunks(
|
||||
},
|
||||
],
|
||||
error: {
|
||||
message: publicErrorMessage,
|
||||
message: errorMsg,
|
||||
type: statusMapping.responses.type,
|
||||
code: statusMapping.responses.code,
|
||||
},
|
||||
|
||||
69
tests/fixtures/adapta-web-stream-error-boundary.fixture.ts
vendored
Normal file
69
tests/fixtures/adapta-web-stream-error-boundary.fixture.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
assert.ok(process.env.DATA_DIR, "the subprocess fixture requires an isolated DATA_DIR");
|
||||
assert.ok(
|
||||
process.env.OMNIROUTE_PLUGINS_DIR,
|
||||
"the subprocess fixture requires an isolated OMNIROUTE_PLUGINS_DIR"
|
||||
);
|
||||
assert.equal(process.env.HOME, undefined, "the subprocess must not inherit HOME");
|
||||
assert.equal(process.env.CODEX_HOME, undefined, "the subprocess must not inherit CODEX_HOME");
|
||||
|
||||
const { AdaptaWebExecutor } = await import("../../open-sse/executors/adapta-web.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test("terminates an upstream error event without exposing its text in the public SSE", async () => {
|
||||
const hostileError =
|
||||
"SQLSTATE 42P01 at /srv/omniroute/private.ts:91 — Authorization: Bearer secret-token";
|
||||
const requestedUrls: string[] = [];
|
||||
const logMessages: string[] = [];
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
requestedUrls.push(url);
|
||||
|
||||
if (url.endsWith("/v1/client")) {
|
||||
return Response.json({
|
||||
response: { sessions: [{ id: "session-stream-error", status: "active" }] },
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/tokens")) {
|
||||
return Response.json({ jwt: "eyJ.test-session.jwt" });
|
||||
}
|
||||
|
||||
return new Response(`data: ${JSON.stringify({ type: "error", errorText: hostileError })}\n\n`, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const executor = new AdaptaWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "adapta-one",
|
||||
body: { messages: [{ role: "user", content: "hello" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "__client=unique-stream-error-cookie" },
|
||||
signal: null,
|
||||
log: {
|
||||
info: (_tag, message) => logMessages.push(message),
|
||||
warn: (_tag, message) => logMessages.push(message),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 200);
|
||||
assert.equal(result.response.headers.get("content-type"), "text/event-stream");
|
||||
|
||||
const publicSse = await result.response.text();
|
||||
assert.equal(requestedUrls.length, 3);
|
||||
assert.match(publicSse, /"content":"\\n\\n\[Erro: Adapta upstream error\]"/);
|
||||
assert.match(publicSse, /"finish_reason":"stop"/);
|
||||
assert.match(publicSse, /data: \[DONE\]/);
|
||||
assert.doesNotMatch(publicSse, /SQLSTATE|\/srv\/omniroute|secret-token/);
|
||||
assert.doesNotMatch(logMessages.join("\n"), /SQLSTATE|\/srv\/omniroute|secret-token/);
|
||||
});
|
||||
@@ -1,211 +0,0 @@
|
||||
// This suite owns process-wide DATA_DIR, plugin, logger, and DB state. It must run only inside
|
||||
// the subprocess launched by tests/unit/stream-handler-public-error-boundary.test.ts.
|
||||
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 originalDataDir = process.env.DATA_DIR;
|
||||
const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR;
|
||||
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-public-error-"));
|
||||
const TEST_DATA_DIR = path.join(testRoot, "data");
|
||||
const TEST_PLUGINS_DIR = path.join(testRoot, "plugins");
|
||||
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, callLogs, artifactWriter, loggerResource, streamHandler, { FORMATS }] =
|
||||
await Promise.all([
|
||||
import("../../src/lib/db/core.ts"),
|
||||
import("../../src/lib/usage/callLogs.ts"),
|
||||
import("../../src/lib/usage/callLogArtifactWriter.ts"),
|
||||
import("../../src/shared/utils/loggerResource.ts"),
|
||||
import("../../open-sse/utils/streamHandler.ts"),
|
||||
import("../../open-sse/translator/formats.ts"),
|
||||
]);
|
||||
const { createStreamController, pipeWithDisconnect } = streamHandler;
|
||||
|
||||
const SECRET = "sk-live-streamhandler-secret-123456";
|
||||
const API_KEY = "provider-key-streamhandler-654321";
|
||||
const PRIVATE_PATH = "/srv/omniroute/private/provider.ts:42:9";
|
||||
const RAW_MESSAGE =
|
||||
`Upstream failed at ${PRIVATE_PATH} Authorization: Bearer ${SECRET} api_key=${API_KEY}` +
|
||||
`\n at dispatch (/srv/omniroute/private/dispatcher.ts:88:3)`;
|
||||
|
||||
test.after(async () => {
|
||||
assert.equal(await callLogs.waitForCallLogSaves(3_000), true);
|
||||
await artifactWriter.closeCallLogArtifactWriter();
|
||||
core.resetDbInstance();
|
||||
await loggerResource.closeSharedLoggerResource();
|
||||
|
||||
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("fixture binds all persistent state to its process-owned directories", () => {
|
||||
assert.equal(core.DATA_DIR, TEST_DATA_DIR);
|
||||
assert.equal(core.SQLITE_FILE, path.join(TEST_DATA_DIR, "storage.sqlite"));
|
||||
assert.equal(process.env.DATA_DIR, TEST_DATA_DIR);
|
||||
assert.equal(process.env.OMNIROUTE_PLUGINS_DIR, TEST_PLUGINS_DIR);
|
||||
assert.equal(fs.existsSync(TEST_DATA_DIR), true);
|
||||
assert.equal(fs.existsSync(TEST_PLUGINS_DIR), true);
|
||||
});
|
||||
|
||||
test("OpenAI stream failures keep raw diagnostics internal and sanitize the public wire", async () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 502 });
|
||||
const source = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.error(upstreamError);
|
||||
},
|
||||
});
|
||||
let internalMessage = "";
|
||||
|
||||
const stream = pipeWithDisconnect(
|
||||
new Response(source),
|
||||
new TransformStream<Uint8Array, Uint8Array>(),
|
||||
createStreamController({
|
||||
clientResponseFormat: FORMATS.OPENAI,
|
||||
onError(event) {
|
||||
internalMessage = event.message;
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{ stallTimeoutMs: 0 }
|
||||
);
|
||||
const publicWire = await new Response(stream).text();
|
||||
|
||||
assert.equal(internalMessage, RAW_MESSAGE, "failure classification must retain the raw message");
|
||||
assert.match(publicWire, /"finish_reason":"error"/);
|
||||
assert.match(publicWire, /"code":"server_error"/);
|
||||
assert.match(publicWire, /\[DONE\]/);
|
||||
assert.doesNotMatch(publicWire, new RegExp(SECRET));
|
||||
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
|
||||
assert.match(publicWire, /Authorization: \[REDACTED\]/);
|
||||
assert.match(publicWire, /<path>/);
|
||||
});
|
||||
|
||||
test("Responses stream failures preserve the failure event shape without leaking diagnostics", async () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 429 });
|
||||
const source = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.error(upstreamError);
|
||||
},
|
||||
});
|
||||
let internalError: unknown;
|
||||
|
||||
const stream = pipeWithDisconnect(
|
||||
new Response(source),
|
||||
new TransformStream<Uint8Array, Uint8Array>(),
|
||||
createStreamController({
|
||||
clientResponseFormat: FORMATS.OPENAI_RESPONSES,
|
||||
onError(event) {
|
||||
internalError = event.error;
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{ stallTimeoutMs: 0 }
|
||||
);
|
||||
const publicWire = await new Response(stream).text();
|
||||
|
||||
assert.equal(internalError, upstreamError, "the original error object must reach classification");
|
||||
assert.match(publicWire, /event: response\.failed/);
|
||||
assert.match(publicWire, /"type":"response\.failed"/);
|
||||
assert.match(publicWire, /"type":"rate_limit_error"/);
|
||||
assert.match(publicWire, /"code":"rate_limit_exceeded"/);
|
||||
assert.doesNotMatch(publicWire, new RegExp(SECRET));
|
||||
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
|
||||
assert.match(publicWire, /Authorization: \[REDACTED\]/);
|
||||
assert.match(publicWire, /<path>/);
|
||||
});
|
||||
|
||||
test("Claude stream failures preserve error and stop events without leaking diagnostics", async () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 403 });
|
||||
const source = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.error(upstreamError);
|
||||
},
|
||||
});
|
||||
let internalStatusCode = 0;
|
||||
|
||||
const stream = pipeWithDisconnect(
|
||||
new Response(source),
|
||||
new TransformStream<Uint8Array, Uint8Array>(),
|
||||
createStreamController({
|
||||
clientResponseFormat: FORMATS.CLAUDE,
|
||||
onError(event) {
|
||||
internalStatusCode = event.statusCode;
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{ stallTimeoutMs: 0 }
|
||||
);
|
||||
const publicWire = await new Response(stream).text();
|
||||
|
||||
assert.equal(internalStatusCode, 403);
|
||||
assert.match(publicWire, /event: error/);
|
||||
assert.match(publicWire, /"type":"permission_error"/);
|
||||
assert.match(publicWire, /event: message_stop/);
|
||||
assert.doesNotMatch(publicWire, new RegExp(SECRET));
|
||||
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
|
||||
assert.match(publicWire, /Authorization: \[REDACTED\]/);
|
||||
assert.match(publicWire, /<path>/);
|
||||
});
|
||||
|
||||
test("stream diagnostics sanitize logs while callbacks retain the original failure", () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 502 });
|
||||
const originalLog = console.log;
|
||||
const logLines: string[] = [];
|
||||
let internalError: unknown;
|
||||
console.log = (...args: unknown[]) => {
|
||||
logLines.push(args.map(String).join(" "));
|
||||
};
|
||||
|
||||
try {
|
||||
createStreamController({
|
||||
provider: "test-provider",
|
||||
model: "test-model",
|
||||
onError(event) {
|
||||
internalError = event.error;
|
||||
return true;
|
||||
},
|
||||
}).handleError(upstreamError);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
|
||||
const logs = logLines.join("\n");
|
||||
assert.equal(internalError, upstreamError);
|
||||
assert.match(logs, /error: Upstream failed at <path>/);
|
||||
assert.match(logs, /Authorization: \[REDACTED\]/);
|
||||
assert.doesNotMatch(logs, new RegExp(SECRET));
|
||||
assert.doesNotMatch(logs, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(logs, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(logs, /dispatcher\.ts/);
|
||||
});
|
||||
|
||||
test("client disconnects stay outside the provider-failure callback", () => {
|
||||
let providerFailureRecorded = false;
|
||||
const controller = createStreamController({
|
||||
onError() {
|
||||
providerFailureRecorded = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
controller.handleError(new DOMException("request_signal_aborted", "AbortError"));
|
||||
|
||||
assert.equal(providerFailureRecorded, false);
|
||||
assert.equal(controller.signal.aborted, false);
|
||||
});
|
||||
97
tests/unit/executor-adapta-web-stream-error.test.ts
Normal file
97
tests/unit/executor-adapta-web-stream-error.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const FIXTURE = fileURLToPath(
|
||||
new URL("../fixtures/adapta-web-stream-error-boundary.fixture.ts", import.meta.url)
|
||||
);
|
||||
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const SYNTHETIC_API_KEY_SECRET =
|
||||
"adapta-stream-boundary-test-secret-00000000000000000000000000000000";
|
||||
|
||||
type FixtureResult = {
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
function runIsolatedFixture(testRoot: string): Promise<FixtureResult> {
|
||||
const dataDir = path.join(testRoot, "data");
|
||||
const pluginsDir = path.join(testRoot, "plugins");
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.mkdirSync(pluginsDir, { recursive: true });
|
||||
|
||||
const childEnv: NodeJS.ProcessEnv = {
|
||||
PATH: process.env.PATH,
|
||||
NODE_PATH: process.env.NODE_PATH,
|
||||
LANG: process.env.LANG ?? "C.UTF-8",
|
||||
LC_ALL: process.env.LC_ALL,
|
||||
TZ: process.env.TZ ?? "UTC",
|
||||
TMPDIR: process.env.TMPDIR ?? os.tmpdir(),
|
||||
NODE_ENV: "test",
|
||||
APP_LOG_TO_FILE: "false",
|
||||
API_KEY_SECRET: SYNTHETIC_API_KEY_SECRET,
|
||||
DATA_DIR: dataDir,
|
||||
OMNIROUTE_PLUGINS_DIR: pluginsDir,
|
||||
};
|
||||
delete childEnv.NODE_TEST_CONTEXT;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["--import", "tsx/esm", "--test", FIXTURE], {
|
||||
cwd: REPO_ROOT,
|
||||
env: childEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: 90_000,
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("close", (code, signal) => resolve({ code, signal, stdout, stderr }));
|
||||
});
|
||||
}
|
||||
|
||||
test("Adapta stream errors are sanitized in an isolated executor fixture", async () => {
|
||||
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-adapta-boundary-parent-"));
|
||||
const originalDataDir = process.env.DATA_DIR;
|
||||
const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR;
|
||||
const originalFetch = globalThis.fetch;
|
||||
const eventBusOwner = globalThis as { __omnirouteEventBus?: unknown };
|
||||
const originalEventBus = eventBusOwner.__omnirouteEventBus;
|
||||
|
||||
try {
|
||||
const result = await runIsolatedFixture(testRoot);
|
||||
assert.equal(
|
||||
result.code,
|
||||
0,
|
||||
`isolated Adapta fixture failed (signal=${result.signal ?? "none"})\n` +
|
||||
`stdout:\n${result.stdout}\nstderr:\n${result.stderr}`
|
||||
);
|
||||
assert.equal(result.signal, null);
|
||||
assert.match(result.stdout, /ℹ tests 1/);
|
||||
assert.match(result.stdout, /ℹ pass 1/);
|
||||
assert.match(result.stdout, /ℹ fail 0/);
|
||||
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(SYNTHETIC_API_KEY_SECRET));
|
||||
|
||||
assert.equal(process.env.DATA_DIR, originalDataDir);
|
||||
assert.equal(process.env.OMNIROUTE_PLUGINS_DIR, originalPluginsDir);
|
||||
assert.equal(globalThis.fetch, originalFetch);
|
||||
assert.equal(
|
||||
eventBusOwner.__omnirouteEventBus,
|
||||
originalEventBus,
|
||||
"the subprocess fixture must not replace the parent event bus singleton"
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
});
|
||||
@@ -36,18 +36,25 @@ describe("AdaptaWebExecutor", () => {
|
||||
});
|
||||
|
||||
it("execute returns proper result shape on auth failure", async () => {
|
||||
const executor = new mod.AdaptaWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "adapta-one",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "invalid-jwt" },
|
||||
signal: null,
|
||||
});
|
||||
assert.ok(result.response instanceof Response);
|
||||
assert.ok(typeof result.url === "string");
|
||||
assert.ok(typeof result.headers === "object");
|
||||
assert.ok(result.transformedBody !== undefined);
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => new Response(null, { status: 401 })) as typeof fetch;
|
||||
|
||||
try {
|
||||
const executor = new mod.AdaptaWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "adapta-one",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "invalid-jwt" },
|
||||
signal: null,
|
||||
});
|
||||
assert.ok(result.response instanceof Response);
|
||||
assert.ok(typeof result.url === "string");
|
||||
assert.ok(typeof result.headers === "object");
|
||||
assert.ok(result.transformedBody !== undefined);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("testConnection returns false for invalid credentials", async () => {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const FIXTURE = fileURLToPath(
|
||||
new URL("../fixtures/stream-handler-public-error-boundary.fixture.ts", import.meta.url)
|
||||
);
|
||||
|
||||
const CHILD_RUNTIME_ENV_KEYS = [
|
||||
"PATH",
|
||||
"TMPDIR",
|
||||
"TMP",
|
||||
"TEMP",
|
||||
"SystemRoot",
|
||||
"ComSpec",
|
||||
"PATHEXT",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"TZ",
|
||||
] as const;
|
||||
|
||||
function buildFixtureEnv(): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
NODE_ENV: "test",
|
||||
APP_LOG_TO_FILE: "false",
|
||||
API_KEY_SECRET: "stream-handler-boundary-fixture-secret-20260902",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
NO_COLOR: "1",
|
||||
};
|
||||
|
||||
for (const key of CHILD_RUNTIME_ENV_KEYS) {
|
||||
const value = process.env[key];
|
||||
if (value !== undefined) env[key] = value;
|
||||
}
|
||||
|
||||
// Nested test runners must not inherit the parent runner's recursion marker.
|
||||
delete env.NODE_TEST_CONTEXT;
|
||||
return env;
|
||||
}
|
||||
|
||||
test("generic stream public error boundaries pass in an isolated process", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", "tsx/esm", "--import", "./open-sse/utils/setupPolyfill.ts", "--test", FIXTURE],
|
||||
{
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
env: buildFixtureEnv(),
|
||||
timeout: 120_000,
|
||||
}
|
||||
);
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
|
||||
assert.ifError(result.error);
|
||||
assert.equal(result.signal, null, output.slice(-12_000));
|
||||
assert.equal(result.status, 0, output.slice(-12_000));
|
||||
assert.match(output, /(?:^|\s)tests\s+6(?:\s|$)/m);
|
||||
assert.match(output, /(?:^|\s)pass\s+6(?:\s|$)/m);
|
||||
assert.match(output, /(?:^|\s)fail\s+0(?:\s|$)/m);
|
||||
});
|
||||
@@ -256,8 +256,7 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
|
||||
|
||||
assert.match(text, /event: response\.failed/);
|
||||
assert.match(text, /"type":"response\.failed"/);
|
||||
assert.match(text, /"message":"responses stream"/);
|
||||
assert.doesNotMatch(text, /died/);
|
||||
assert.match(text, /"message":"responses stream\\ndied"/);
|
||||
assert.match(text, /"type":"server_error"/);
|
||||
assert.match(text, /"code":"server_error"/);
|
||||
assert.doesNotMatch(text, /chat\.completion\.chunk/);
|
||||
@@ -265,7 +264,7 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
|
||||
assert.doesNotMatch(text, /\[DONE\]/);
|
||||
});
|
||||
|
||||
test("createDisconnectAwareStream strips multiline diagnostic tails from Responses errors", async () => {
|
||||
test("createDisconnectAwareStream keeps newlines escaped inside SSE data fields", async () => {
|
||||
const upstreamError = Object.assign(new Error("line one\nline two\rline three"), {
|
||||
statusCode: 400,
|
||||
});
|
||||
@@ -291,9 +290,9 @@ test("createDisconnectAwareStream strips multiline diagnostic tails from Respons
|
||||
const text = await readStreamText(stream);
|
||||
|
||||
assert.match(text, /^event: response\.failed\ndata: \{"type":"response\.failed"/);
|
||||
assert.match(text, /"message":"line one"/);
|
||||
assert.doesNotMatch(text, /line two/);
|
||||
assert.doesNotMatch(text, /line three/);
|
||||
assert.match(text, /"message":"line one\\nline two\\rline three"/);
|
||||
assert.doesNotMatch(text, /^line two/m);
|
||||
assert.doesNotMatch(text, /^line three/m);
|
||||
});
|
||||
|
||||
test("createDisconnectAwareStream treats legacy OpenAI response format alias as Responses", async () => {
|
||||
@@ -361,7 +360,7 @@ test("createDisconnectAwareStream emits Claude SSE errors for Claude clients", a
|
||||
assert.doesNotMatch(text, /\[DONE\]/);
|
||||
});
|
||||
|
||||
test("createDisconnectAwareStream strips multiline diagnostic tails from Claude errors", async () => {
|
||||
test("createDisconnectAwareStream keeps newlines escaped for Claude SSE errors", async () => {
|
||||
const upstreamError = Object.assign(new Error("claude line one\nclaude line two"), {
|
||||
statusCode: 502,
|
||||
});
|
||||
@@ -387,8 +386,8 @@ test("createDisconnectAwareStream strips multiline diagnostic tails from Claude
|
||||
const text = await readStreamText(stream);
|
||||
|
||||
assert.match(text, /^event: error\ndata: \{"type":"error"/);
|
||||
assert.match(text, /"message":"claude line one"/);
|
||||
assert.doesNotMatch(text, /claude line two/);
|
||||
assert.match(text, /"message":"claude line one\\nclaude line two"/);
|
||||
assert.doesNotMatch(text, /^claude line two/m);
|
||||
});
|
||||
|
||||
// #7699/#7816 — heuristic is scoped to FORMATS.CLAUDE (/v1/messages); a
|
||||
|
||||
Reference in New Issue
Block a user