diff --git a/tests/fixtures/adapta-web-stream-error-boundary.fixture.ts b/tests/fixtures/adapta-web-stream-error-boundary.fixture.ts new file mode 100644 index 0000000000..02c7e6f07e --- /dev/null +++ b/tests/fixtures/adapta-web-stream-error-boundary.fixture.ts @@ -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/); +}); diff --git a/tests/unit/executor-adapta-web-stream-error.test.ts b/tests/unit/executor-adapta-web-stream-error.test.ts index 23cec6c610..f17667b92a 100644 --- a/tests/unit/executor-adapta-web-stream-error.test.ts +++ b/tests/unit/executor-adapta-web-stream-error.test.ts @@ -1,70 +1,97 @@ import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +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 testRoot = mkdtempSync(join(tmpdir(), "omniroute-adapta-stream-error-")); -process.env.DATA_DIR = join(testRoot, "data"); -process.env.OMNIROUTE_PLUGINS_DIR = join(testRoot, "plugins"); -mkdirSync(process.env.DATA_DIR, { recursive: true }); -mkdirSync(process.env.OMNIROUTE_PLUGINS_DIR, { recursive: true }); +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"; -const { AdaptaWebExecutor } = await import("../../open-sse/executors/adapta-web.ts"); +type FixtureResult = { + code: number | null; + signal: NodeJS.Signals | null; + stdout: string; + stderr: string; +}; -const originalFetch = globalThis.fetch; +function runIsolatedFixture(testRoot: string): Promise { + const dataDir = path.join(testRoot, "data"); + const pluginsDir = path.join(testRoot, "plugins"); + fs.mkdirSync(dataDir, { recursive: true }); + fs.mkdirSync(pluginsDir, { recursive: true }); -test.afterEach(() => { - globalThis.fetch = originalFetch; -}); + 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; -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" }, + 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, }); - }) 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), - }, + 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 })); }); +} - assert.equal(result.response.status, 200); - assert.equal(result.response.headers.get("content-type"), "text/event-stream"); +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; - 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/); + 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 }); + } });