From 2f6fdf16c75f0a2a0a7beb5bb30617aef2f97d90 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 3 Sep 2026 20:59:14 -0300 Subject: [PATCH] fix(codex): close response failure boundary (#12444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validado em lote numa worktree combinada com os 14 PRs desta campanha de error-boundary sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **120/120** nos 23 arquivos de teste que os PRs trazem. Um ponto que só apareceu no tree combinado: **#12465 e #12466 criam o mesmo arquivo novo** `open-sse/utils/streamReadiness.ts` (que não existe no tip) com desenhos divergentes de cancelamento — `cancelled` + `releaseLock` imediato num, `readInFlight`/`cancelRequested` com `cancelReader` fire-and-forget no outro. Adotei a versão do #12466, que difere e defere o release do lock para quando a leitura em voo termina, e validei a escolha rodando as suítes dos **dois** PRs contra ela: 21/21 no readiness compartilhado e 22/22 incluindo o boundary do Perplexity. --- open-sse/executors/codex.ts | 10 +- open-sse/utils/codexPublicError.ts | 110 +++++ open-sse/vendor/codex-chatgpt-web/bridge.ts | 21 +- .../codex-response-failed-boundary.fixture.ts | 376 ++++++++++++++++++ .../codex-response-failed-boundary.test.ts | 73 ++++ 5 files changed, 582 insertions(+), 8 deletions(-) create mode 100644 open-sse/utils/codexPublicError.ts create mode 100644 tests/fixtures/codex-response-failed-boundary.fixture.ts create mode 100644 tests/unit/codex-response-failed-boundary.test.ts diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 4701f175c2..1194fe4dee 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -38,6 +38,7 @@ import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts"; import { normalizeCodexVerbosity } from "../services/codexVerbosity.ts"; import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts"; import { CORS_HEADERS } from "../utils/cors.ts"; +import { projectCodexPublicError } from "../utils/codexPublicError.ts"; import { errorResponse } from "../utils/error.ts"; import { normalizeCodexResponsesInput } from "../utils/responsesInputNormalization.ts"; import * as prl from "../utils/providerRequestLogging.ts"; @@ -493,7 +494,6 @@ function toCodexResponseFailedEvent(parsed: Record): Record = { code, message }; const explicitStatus = toStatusCode(parsed.status_code) ?? toStatusCode(parsed.status) ?? @@ -503,8 +503,10 @@ function toCodexResponseFailedEvent(parsed: Record): Record = { + ...projectCodexPublicError({ status: statusCode, code, type }), + }; - if (type) error.type = type; if (statusCode !== null) error.status_code = statusCode; return { @@ -955,7 +957,7 @@ export class CodexExecutor extends BaseExecutor { } }; - const failController = (code: string, message: string) => { + const failController = (code: string, _message: string) => { if (closed) return; const controller = streamController; const payload = JSON.stringify({ @@ -963,7 +965,7 @@ export class CodexExecutor extends BaseExecutor { response: { id: null, status: "failed", - error: { code, message }, + error: projectCodexPublicError({ status: 502, code, type: "provider_error" }), }, }); try { diff --git a/open-sse/utils/codexPublicError.ts b/open-sse/utils/codexPublicError.ts new file mode 100644 index 0000000000..6a8c92209c --- /dev/null +++ b/open-sse/utils/codexPublicError.ts @@ -0,0 +1,110 @@ +import { sanitizeErrorMessage } from "./error.ts"; + +export const CODEX_PUBLIC_ERROR_MESSAGE = sanitizeErrorMessage("Codex provider request failed"); + +export interface CodexPublicError { + message: string; + type: string; + code: string; +} + +interface CodexPublicErrorInput { + status?: number | null; + type?: unknown; + code?: unknown; +} + +interface CodexPublicErrorRule { + type: string; + allowsStatus: (status: number) => boolean; +} + +const exactStatuses = + (...statuses: number[]) => + (status: number): boolean => + statuses.includes(status); + +const CODEX_PUBLIC_ERROR_RULES = new Map([ + ["browser_stream_inconsistent", { type: "server_error", allowsStatus: exactStatuses(502) }], + ["chatgpt_session_expired", { type: "authentication_error", allowsStatus: exactStatuses(401) }], + ["chatgpt_submission_ambiguous", { type: "server_error", allowsStatus: exactStatuses(502) }], + ["chatgpt_submitted_turn_failed", { type: "server_error", allowsStatus: exactStatuses(502) }], + ["chatgpt_subscription_unavailable", { type: "server_error", allowsStatus: exactStatuses(503) }], + ["client_cancelled", { type: "invalid_request_error", allowsStatus: exactStatuses(499) }], + ["client_closed_request", { type: "invalid_request_error", allowsStatus: exactStatuses(499) }], + ["codex_app_server_turn_failed", { type: "provider_error", allowsStatus: exactStatuses(502) }], + [ + "compaction_control_unavailable", + { type: "invalid_request_error", allowsStatus: exactStatuses(409) }, + ], + [ + "compaction_handoff_failed", + { type: "invalid_request_error", allowsStatus: exactStatuses(409) }, + ], + [ + "compaction_source_unavailable", + { type: "invalid_request_error", allowsStatus: exactStatuses(409) }, + ], + ["connector_not_found", { type: "connector_error", allowsStatus: exactStatuses(424) }], + [ + "context_length_exceeded", + { type: "invalid_request_error", allowsStatus: exactStatuses(400, 413) }, + ], + ["insufficient_quota", { type: "insufficient_quota", allowsStatus: exactStatuses(429) }], + ["invalid_api_key", { type: "authentication_error", allowsStatus: exactStatuses(401) }], + ["invalid_output_schema", { type: "invalid_request_error", allowsStatus: exactStatuses(400) }], + ["invalid_request_error", { type: "invalid_request_error", allowsStatus: exactStatuses(400) }], + ["multipart_protocol_violation", { type: "server_error", allowsStatus: exactStatuses(502) }], + ["origin_rejected", { type: "invalid_request_error", allowsStatus: exactStatuses(403) }], + ["permission_denied", { type: "permission_error", allowsStatus: exactStatuses(403) }], + ["prompt_attachment_integrity", { type: "server_error", allowsStatus: exactStatuses(502) }], + ["rate_limit_exceeded", { type: "rate_limit_error", allowsStatus: exactStatuses(429) }], + ["server_is_overloaded", { type: "server_error", allowsStatus: exactStatuses(503) }], + [ + "structured_output_validation_failed", + { type: "server_error", allowsStatus: exactStatuses(502) }, + ], + ["subscription_required", { type: "permission_error", allowsStatus: exactStatuses(403) }], + [ + "upstream_server_error", + { + type: "server_error", + allowsStatus: (status) => status >= 500 && status <= 599 && status !== 503, + }, + ], + [ + "upstream_websocket_connect_failed", + { type: "provider_error", allowsStatus: exactStatuses(502) }, + ], + ["upstream_websocket_error", { type: "provider_error", allowsStatus: exactStatuses(502) }], + ["usage_limit_reached", { type: "rate_limit_error", allowsStatus: exactStatuses(429) }], +]); + +function defaultPublicClassification(status: number): Pick { + if (status === 429) return { type: "rate_limit_error", code: "rate_limit_exceeded" }; + if (status === 401) return { type: "authentication_error", code: "invalid_api_key" }; + if (status === 403) return { type: "permission_error", code: "permission_denied" }; + if (status === 499) return { type: "invalid_request_error", code: "client_closed_request" }; + if (status === 503) return { type: "server_error", code: "server_is_overloaded" }; + if (status >= 500) return { type: "server_error", code: "upstream_server_error" }; + return { type: "invalid_request_error", code: "invalid_request_error" }; +} + +/** + * Project an internally classified Codex failure onto its public Responses contract. + * + * Upstream message, code, and type fields are untrusted. The public message is fixed, + * while code/type retain only closed, protocol-level identifiers already produced by + * OmniRoute. Everything else falls back to the HTTP status classification. + */ +export function projectCodexPublicError(input: CodexPublicErrorInput): CodexPublicError { + const status = + typeof input.status === "number" && Number.isInteger(input.status) ? input.status : 502; + const fallback = defaultPublicClassification(status); + const rule = + typeof input.code === "string" ? CODEX_PUBLIC_ERROR_RULES.get(input.code) : undefined; + if (!rule || !rule.allowsStatus(status)) { + return { message: CODEX_PUBLIC_ERROR_MESSAGE, ...fallback }; + } + return { message: CODEX_PUBLIC_ERROR_MESSAGE, type: rule.type, code: input.code as string }; +} diff --git a/open-sse/vendor/codex-chatgpt-web/bridge.ts b/open-sse/vendor/codex-chatgpt-web/bridge.ts index 38eacd167c..a6f6244969 100644 --- a/open-sse/vendor/codex-chatgpt-web/bridge.ts +++ b/open-sse/vendor/codex-chatgpt-web/bridge.ts @@ -5,6 +5,7 @@ import type { CodexProviderContinuationState, CodexUsage, } from "./types"; +import { projectCodexPublicError } from "../../utils/codexPublicError"; import { adapterFailureFromMessage, classifyError, type CodexErrorPayload } from "./lib/errors"; import { encodeCompactionSummary } from "./responses/compaction"; import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope"; @@ -46,7 +47,8 @@ function responsesUsage(usage: CodexUsage | undefined): Record } function responseError(status: number, type: string, message: string): CodexErrorPayload { - return classifyError(status, type, message); + const classified = classifyError(status, type, message); + return projectCodexPublicError({ status, type: classified.type, code: classified.code }); } function adapterFailureFromEvent(event: Extract): { @@ -54,14 +56,25 @@ function adapterFailureFromEvent(event: Extract error: CodexErrorPayload; } { if (event.status === undefined && event.errorType === undefined && event.code === undefined) { - return adapterFailureFromMessage(event.message); + const fallback = adapterFailureFromMessage(event.message); + return { + httpStatus: fallback.httpStatus, + error: projectCodexPublicError({ + status: fallback.httpStatus, + type: fallback.error.type, + code: fallback.error.code, + }), + }; } const fallback = adapterFailureFromMessage(event.message); const httpStatus = event.status ?? fallback.httpStatus; const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message); if (event.errorType !== undefined) error.type = event.errorType; if (event.code !== undefined) error.code = event.code; - return { httpStatus, error }; + return { + httpStatus, + error: projectCodexPublicError({ status: httpStatus, type: error.type, code: error.code }), + }; } export { adapterFailureFromMessage } from "./lib/errors"; @@ -1314,7 +1327,7 @@ export function buildResponseJSON( } export function formatErrorResponse(status: number, type: string, message: string): Response { - return new Response(JSON.stringify({ error: classifyError(status, type, message) }), { + return new Response(JSON.stringify({ error: responseError(status, type, message) }), { status, headers: { "Content-Type": "application/json" }, }); diff --git a/tests/fixtures/codex-response-failed-boundary.fixture.ts b/tests/fixtures/codex-response-failed-boundary.fixture.ts new file mode 100644 index 0000000000..eba763b40a --- /dev/null +++ b/tests/fixtures/codex-response-failed-boundary.fixture.ts @@ -0,0 +1,376 @@ +// This suite intentionally owns process-wide DATA_DIR, plugin, and DB state. It must run only +// inside the subprocess launched by tests/unit/codex-response-failed-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"; + +import type { CodexWreqWebSocket } from "../../open-sse/executors/codex/appServerClient.ts"; +import type { AdapterEvent } from "../../open-sse/vendor/codex-chatgpt-web/types.ts"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-boundary-data-")); +const TEST_PLUGINS_DIR = fs.mkdtempSync( + path.join(os.tmpdir(), "omniroute-codex-boundary-plugins-") +); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR; +process.env.APP_LOG_TO_FILE = "false"; + +const { CodexExecutor, __setCodexWebSocketTransportForTesting, encodeResponseSseEvent } = + await import("../../open-sse/executors/codex.ts"); +const { CodexAppServerExecutor } = await import("../../open-sse/executors/codex-app-server.ts"); +const { bridgeToResponsesSSE, buildResponseJSON } = + await import("../../open-sse/vendor/codex-chatgpt-web/bridge.ts"); +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + +const PUBLIC_MESSAGE = "Codex provider request failed"; +const HOSTILE_MESSAGE = + "token=codex-secret-value at /srv/omniroute/private/config.json\nforged-log: admin=true"; + +type FailedPayload = { + type: "response.failed"; + response: { + error: { + code: string | null; + message: string; + status_code?: number; + type?: string; + }; + }; +}; + +function responseFailedPayload(sse: string): FailedPayload { + for (const line of sse.split("\n")) { + if (!line.startsWith("data: ") || line === "data: [DONE]") continue; + const parsed = JSON.parse(line.slice("data: ".length)) as Record; + if (parsed.type === "response.failed") return parsed as FailedPayload; + } + assert.fail(`response.failed frame missing from: ${sse}`); +} + +function assertPublicFailure( + payload: FailedPayload, + expected: { code: string; type: string; statusCode?: number } +): void { + assert.equal(payload.response.error.message, PUBLIC_MESSAGE); + assert.equal(payload.response.error.code, expected.code); + assert.equal(payload.response.error.type, expected.type); + if (expected.statusCode !== undefined) { + assert.equal(payload.response.error.status_code, expected.statusCode); + } + assert.ok(!JSON.stringify(payload).includes(HOSTILE_MESSAGE)); + assert.ok(!JSON.stringify(payload).includes("codex-secret-value")); + assert.ok(!JSON.stringify(payload).includes("/srv/omniroute/private")); +} + +async function executeCodexWebSocketFailure( + websocket: Parameters[0] +): Promise { + __setCodexWebSocketTransportForTesting(websocket); + try { + const result = await new CodexExecutor().execute({ + model: "gpt-5.5", + body: { model: "gpt-5.5", input: "hello" }, + stream: true, + credentials: { + accessToken: "test-token", + providerSpecificData: { codexTransport: "websocket" }, + }, + }); + return await result.response.text(); + } finally { + __setCodexWebSocketTransportForTesting(undefined); + } +} + +async function executeAppServerFailure(stream: boolean): Promise { + const socket: CodexWreqWebSocket = { + send(data: string) { + const frame = JSON.parse(data) as Record; + if (frame.id == null || typeof frame.method !== "string") return; + queueMicrotask(() => { + if (frame.method === "thread/start") { + socket.onmessage?.({ + data: JSON.stringify({ + jsonrpc: "2.0", + id: frame.id, + result: { thread: { id: "thread-public-boundary" } }, + }), + }); + return; + } + if (frame.method === "turn/start") { + socket.onmessage?.({ + data: JSON.stringify({ + jsonrpc: "2.0", + id: frame.id, + result: { turn: { id: "turn-public-boundary", status: "inProgress" } }, + }), + }); + setTimeout(() => { + socket.onmessage?.({ + data: JSON.stringify({ + jsonrpc: "2.0", + method: "error", + params: { error: { message: HOSTILE_MESSAGE } }, + }), + }); + }, 0); + return; + } + socket.onmessage?.({ + data: JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: {} }), + }); + }); + }, + close() {}, + onmessage: null, + onerror: null, + onclose: null, + }; + const executor = new CodexAppServerExecutor({ websocketFn: async () => socket }); + const result = await executor.execute({ + model: "gpt-5.5", + body: { input: "hello" }, + stream, + credentials: { + providerSpecificData: { + codexTransport: "app-server", + codexAppServerUrl: "ws://codex-app-server.test:1456", + codexAppServerToken: "test-app-server-token", + }, + }, + }); + return result.response; +} + +test.after(() => { + __setCodexWebSocketTransportForTesting(undefined); + resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.rmSync(TEST_PLUGINS_DIR, { recursive: true, force: true }); +}); + +test("Codex same-format error event emits only a fixed public failure contract", () => { + const result = encodeResponseSseEvent( + JSON.stringify({ + type: "error", + status_code: 502, + error: { + code: "secret_backend_code_9182", + type: "secret_backend_type_7731", + message: HOSTILE_MESSAGE, + }, + }) + ); + + assert.equal(result.terminal, true); + assertPublicFailure(responseFailedPayload(result.sse), { + code: "upstream_server_error", + type: "server_error", + statusCode: 502, + }); +}); + +test("Codex same-format quota classification survives while its raw message does not", () => { + const result = encodeResponseSseEvent( + JSON.stringify({ + type: "response.failed", + response: { + status: "failed", + error: { code: "usage_limit_reached", message: HOSTILE_MESSAGE }, + }, + }) + ); + + assertPublicFailure(responseFailedPayload(result.sse), { + code: "usage_limit_reached", + type: "rate_limit_error", + statusCode: 429, + }); +}); + +test("Codex same-format failures reject contradictory allowlisted status, code and type", () => { + const wrongStatus = responseFailedPayload( + encodeResponseSseEvent( + JSON.stringify({ + type: "response.failed", + status_code: 502, + response: { + status: "failed", + error: { + code: "invalid_api_key", + type: "rate_limit_error", + message: HOSTILE_MESSAGE, + }, + }, + }) + ).sse + ); + assertPublicFailure(wrongStatus, { + code: "upstream_server_error", + type: "server_error", + statusCode: 502, + }); + + const wrongType = responseFailedPayload( + encodeResponseSseEvent( + JSON.stringify({ + type: "response.failed", + status_code: 401, + response: { + status: "failed", + error: { + code: "invalid_api_key", + type: "rate_limit_error", + message: HOSTILE_MESSAGE, + }, + }, + }) + ).sse + ); + assertPublicFailure(wrongType, { + code: "invalid_api_key", + type: "authentication_error", + statusCode: 401, + }); +}); + +test("Codex WebSocket in-flight error event cannot expose transport details", async () => { + const socket = { + send() { + queueMicrotask(() => socket.onerror?.({ message: HOSTILE_MESSAGE })); + }, + close() {}, + onmessage: null as ((event: { data: unknown }) => void) | null, + onerror: null as ((event: { message?: string }) => void) | null, + onclose: null as (() => void) | null, + }; + const sse = await executeCodexWebSocketFailure(async () => socket); + + assertPublicFailure(responseFailedPayload(sse), { + code: "upstream_websocket_error", + type: "provider_error", + }); +}); + +test("Codex WebSocket connection failure cannot expose exception details", async () => { + const sse = await executeCodexWebSocketFailure(async () => { + throw new Error(HOSTILE_MESSAGE); + }); + + assertPublicFailure(responseFailedPayload(sse), { + code: "upstream_websocket_connect_failed", + type: "provider_error", + }); +}); + +test("Codex App Server streaming failure is projected before the HTTP 200 SSE boundary", async () => { + const response = await executeAppServerFailure(true); + assert.equal(response.status, 200); + + assertPublicFailure(responseFailedPayload(await response.text()), { + code: "codex_app_server_turn_failed", + type: "provider_error", + }); +}); + +test("Codex App Server non-streaming failure is projected before the HTTP 200 JSON boundary", async () => { + const response = await executeAppServerFailure(false); + assert.equal(response.status, 200); + const body = (await response.json()) as FailedPayload["response"] & { status: string }; + + assert.equal(body.status, "failed"); + assertPublicFailure( + { type: "response.failed", response: body }, + { + code: "codex_app_server_turn_failed", + type: "provider_error", + } + ); +}); + +test("ChatGPT Web Playwright adapter failures keep safe routing metadata without raw text", async () => { + async function* browserEvents(): AsyncGenerator { + yield { + type: "error", + message: HOSTILE_MESSAGE, + status: 502, + errorType: "server_error", + code: "chatgpt_submission_ambiguous", + retryable: false, + }; + } + + const sse = await new Response(bridgeToResponsesSSE(browserEvents(), "gpt-5.5")).text(); + assertPublicFailure(responseFailedPayload(sse), { + code: "chatgpt_submission_ambiguous", + type: "server_error", + }); +}); + +test("Codex bridge projects message-only adapter failures before SSE serialization", async () => { + async function* messageOnlyEvents(): AsyncGenerator { + yield { type: "error", message: HOSTILE_MESSAGE }; + } + + const sse = await new Response(bridgeToResponsesSSE(messageOnlyEvents(), "gpt-5.5")).text(); + assertPublicFailure(responseFailedPayload(sse), { + code: "upstream_server_error", + type: "server_error", + }); +}); + +test("Codex batch bridge projects message-only adapter failures before JSON serialization", () => { + const body = buildResponseJSON( + [{ type: "error", message: HOSTILE_MESSAGE }], + "gpt-5.5" + ) as FailedPayload["response"] & { status: string }; + + assert.equal(body.status, "failed"); + assertPublicFailure( + { type: "response.failed", response: body }, + { + code: "upstream_server_error", + type: "server_error", + } + ); +}); + +test("Codex bridge exceptions cannot serialize raw exception messages", async () => { + async function* throwingEvents(): AsyncGenerator { + throw new Error(HOSTILE_MESSAGE); + } + + const sse = await new Response(bridgeToResponsesSSE(throwingEvents(), "gpt-5.5")).text(); + assertPublicFailure(responseFailedPayload(sse), { + code: "upstream_server_error", + type: "server_error", + }); +}); + +test("Codex batch bridge applies the same public failure projector", () => { + const body = buildResponseJSON( + [ + { + type: "error", + message: HOSTILE_MESSAGE, + status: 502, + errorType: "server_error", + code: "chatgpt_submitted_turn_failed", + retryable: false, + }, + ], + "gpt-5.5" + ) as FailedPayload["response"] & { status: string }; + + assert.equal(body.status, "failed"); + assertPublicFailure( + { type: "response.failed", response: body }, + { + code: "chatgpt_submitted_turn_failed", + type: "server_error", + } + ); +}); diff --git a/tests/unit/codex-response-failed-boundary.test.ts b/tests/unit/codex-response-failed-boundary.test.ts new file mode 100644 index 0000000000..51f8ffe59e --- /dev/null +++ b/tests/unit/codex-response-failed-boundary.test.ts @@ -0,0 +1,73 @@ +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/codex-response-failed-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: "codex-boundary-fixture-api-key-secret-20260902", + DISABLE_SQLITE_AUTO_BACKUP: "true", + }; + + for (const key of CHILD_RUNTIME_ENV_KEYS) { + const value = process.env[key]; + if (value !== undefined) env[key] = value; + } + + // A nested test runner must receive its own context instead of inheriting the parent's. + delete env.NODE_TEST_CONTEXT; + return env; +} + +test("Codex public failure boundaries pass in an isolated process", () => { + const result = spawnSync( + process.execPath, + [ + "--import", + "tsx/esm", + "--import", + "./open-sse/utils/setupPolyfill.ts", + "--test", + "--test-force-exit", + FIXTURE, + ], + { + cwd: REPO_ROOT, + encoding: "utf8", + env: buildFixtureEnv(), + timeout: 60_000, + } + ); + + assert.ifError(result.error); + assert.equal( + result.signal, + null, + `isolated Codex boundary fixture terminated by ${result.signal}\n${result.stdout}\n${result.stderr}` + ); + assert.equal( + result.status, + 0, + `isolated Codex boundary fixture failed\n${result.stdout}\n${result.stderr}` + ); +});