mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
The playwright:v1.62.0-noble base ships Chromium as a Chrome for Testing build, which extracts to chrome-linux64/chrome. The CMD's find -path '*/chrome-linux/chrome' matched nothing, $chrome_path came out empty, and the container crash-looped on `exec: --headless=new: not found`. Widening the glob to '*/chrome-linux*/chrome' resolves both the legacy and the Chrome for Testing layout. Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green. Thanks @pacocartones.
975 lines
33 KiB
TypeScript
975 lines
33 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
loadChatGptWebCodexMcpModule,
|
|
resolveChatGptWebCodexMcpEntry,
|
|
} from "../../bin/chatgpt-web-codex-mcp.mjs";
|
|
import {
|
|
hasNativeCodexTurnBinding,
|
|
isCodexOriginatedHeaders,
|
|
isVerifiedNativeCodexRequest,
|
|
} from "../../open-sse/config/codexIdentity.ts";
|
|
import { chatgpt_web_codexProvider } from "../../open-sse/config/providers/registry/chatgpt-web-codex/index.ts";
|
|
import {
|
|
decodeChatGptWebCodexSecrets,
|
|
encodeChatGptWebCodexSecrets,
|
|
} from "../../open-sse/executors/chatgpt-web-codex/credentials.ts";
|
|
import {
|
|
reasoningEffortOf,
|
|
requireChatGptWebCodexRoute,
|
|
} from "../../open-sse/executors/chatgpt-web-codex/models.ts";
|
|
import {
|
|
ensureConnectionStorageState,
|
|
readConnectionStorageState,
|
|
} from "../../open-sse/executors/chatgpt-web-codex/storageState.ts";
|
|
import {
|
|
buildTunnelRuntimeStatusArgs,
|
|
buildTunnelRuntimeStopArgs,
|
|
CHATGPT_WEB_CODEX_TUNNEL_VERSION,
|
|
parseTunnelChecksum,
|
|
parseTunnelRuntimeStatus,
|
|
tunnelClientInstallAction,
|
|
tunnelPlatformAsset,
|
|
} from "../../open-sse/executors/chatgpt-web-codex/tunnelClient.ts";
|
|
import {
|
|
callTurnBroker,
|
|
TurnBroker,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/turn-broker.ts";
|
|
import {
|
|
chatGptPromptFilePayloads,
|
|
insertPlainTextAtComposerSelection,
|
|
mergeChatGptRuntimeStorageState,
|
|
resolveBrowserConfig,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts";
|
|
import {
|
|
loginVerificationMarkerPath,
|
|
writeVerificationMarker,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/browser-login.ts";
|
|
import {
|
|
CHATGPT_CONNECTOR_NAME,
|
|
getConfigDir,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/config.ts";
|
|
import {
|
|
CHATGPT_BIGGER_CONTEXT_PARTS,
|
|
compileChatGptWebPrompt,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/prompt.ts";
|
|
import { parseRequest } from "../../open-sse/vendor/codex-chatgpt-web/responses/parser.ts";
|
|
import {
|
|
expandPreviousResponseInput,
|
|
rememberResponseState,
|
|
resetResponseStateForTests,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/responses/state.ts";
|
|
import type { CodexParsedRequest } from "../../open-sse/vendor/codex-chatgpt-web/types.ts";
|
|
import {
|
|
inputHasSelfContainedCodexContinuation,
|
|
resolveChatGptWebCodexPreviousResponse,
|
|
} from "../../open-sse/executors/chatgpt-web-codex.ts";
|
|
import { checkFallbackError } from "../../open-sse/services/accountFallback.ts";
|
|
import {
|
|
ChatGptTextFeed,
|
|
ChatGptTraceFeed,
|
|
ChatGptTurnSessions,
|
|
} from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/turn-execution.ts";
|
|
import {
|
|
CHATGPT_WEB_CODEX_CONNECTOR_NAME,
|
|
CHATGPT_WEB_CODEX_RUNTIME_HEADED,
|
|
} from "../../src/shared/constants/chatgptWebCodex.ts";
|
|
|
|
test("registers the additive ChatGPT Web Codex provider and current fixed routes", () => {
|
|
assert.equal(CHATGPT_WEB_CODEX_CONNECTOR_NAME, CHATGPT_CONNECTOR_NAME);
|
|
assert.equal(chatgpt_web_codexProvider.id, "chatgpt-web-codex");
|
|
assert.deepEqual(
|
|
chatgpt_web_codexProvider.models?.map((model) => model.id),
|
|
["luna", "think", "instant", "medium", "high", "extra-high", "pro"]
|
|
);
|
|
assert.deepEqual(
|
|
["luna", "think", "instant", "medium", "high", "extra-high", "pro"].map(
|
|
(model) => requireChatGptWebCodexRoute(model).effort
|
|
),
|
|
["low", "medium", "low", "medium", "high", "xhigh", "max"]
|
|
);
|
|
assert.equal(requireChatGptWebCodexRoute("luna").sol, false);
|
|
assert.equal(requireChatGptWebCodexRoute("extra-high").pro, true);
|
|
assert.equal(requireChatGptWebCodexRoute("pro").pro, true);
|
|
});
|
|
|
|
test("runs the ChatGPT browser headed so Cloudflare sees the verified browser shape", () => {
|
|
assert.equal(CHATGPT_WEB_CODEX_RUNTIME_HEADED, true);
|
|
});
|
|
|
|
test("runs the Docker browser headed inside a private Xvfb display", () => {
|
|
const dockerfile = readFileSync(
|
|
join(process.cwd(), "docker/chatgpt-web-codex-browser/Dockerfile"),
|
|
"utf8"
|
|
);
|
|
assert.match(dockerfile, /xvfb-run/);
|
|
assert.doesNotMatch(dockerfile, /--headless(?:=|\s)/);
|
|
assert.match(dockerfile, /-nolisten tcp/);
|
|
});
|
|
|
|
test("#12024 Docker browser find pattern matches both chrome-linux and chrome-linux64 layouts", () => {
|
|
const dockerfile = readFileSync(
|
|
join(process.cwd(), "docker/chatgpt-web-codex-browser/Dockerfile"),
|
|
"utf8"
|
|
);
|
|
const found = dockerfile.match(/find \/ms-playwright -path '([^']+)' -type f/);
|
|
assert.ok(found, "Dockerfile CMD must locate the Chrome binary with a find -path glob");
|
|
const glob = found[1];
|
|
const matcher = new RegExp(
|
|
`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`
|
|
);
|
|
// playwright:v1.62.0-noble ships Chrome for Testing, which extracts to chrome-linux64/.
|
|
assert.match("/ms-playwright/chromium-1234/chrome-linux64/chrome", matcher);
|
|
// Older images keep the legacy chrome-linux/ directory.
|
|
assert.match("/ms-playwright/chromium-1234/chrome-linux/chrome", matcher);
|
|
// The separate headless-shell build ships a different binary name and must not be picked up.
|
|
assert.doesNotMatch(
|
|
"/ms-playwright/chromium_headless_shell-1234/chrome-linux/headless_shell",
|
|
matcher
|
|
);
|
|
});
|
|
|
|
test("preserves browser-verified ChatGPT auth cookies across runtime rotation", () => {
|
|
const cookie = (name: string, value: string) => ({
|
|
name,
|
|
value,
|
|
domain: ".chatgpt.com",
|
|
path: "/",
|
|
expires: -1,
|
|
httpOnly: false,
|
|
secure: true,
|
|
sameSite: "Lax" as const,
|
|
});
|
|
const verified = {
|
|
cookies: [
|
|
cookie("__Secure-next-auth.session-token", "verified-session"),
|
|
cookie("oai-client-session-epoch", "verified-epoch"),
|
|
cookie("oai-did", "stable-device"),
|
|
],
|
|
origins: [],
|
|
};
|
|
const runtime = {
|
|
cookies: [
|
|
cookie("__Secure-next-auth.session-token", "rotated-session"),
|
|
cookie("_puid", "runtime-puid"),
|
|
],
|
|
origins: [{ origin: "https://chatgpt.com", localStorage: [{ name: "runtime", value: "1" }] }],
|
|
};
|
|
|
|
const merged = mergeChatGptRuntimeStorageState(verified, runtime);
|
|
assert.equal(
|
|
merged.cookies.find((entry) => entry.name === "__Secure-next-auth.session-token")?.value,
|
|
"verified-session"
|
|
);
|
|
assert.equal(
|
|
merged.cookies.find((entry) => entry.name === "oai-client-session-epoch")?.value,
|
|
"verified-epoch"
|
|
);
|
|
assert.equal(merged.cookies.find((entry) => entry.name === "_puid")?.value, "runtime-puid");
|
|
assert.deepEqual(merged.origins, runtime.origins);
|
|
});
|
|
|
|
test("loads the TypeScript MCP entrypoint through the Node 26-compatible tsx import hook", async () => {
|
|
const entry = resolveChatGptWebCodexMcpEntry(process.cwd());
|
|
assert.ok(entry?.endsWith(".ts"));
|
|
const module = await loadChatGptWebCodexMcpModule(entry, process.cwd());
|
|
assert.equal(typeof module.runChatGptMcpServer, "function");
|
|
});
|
|
|
|
test("plain-text composer insertion establishes a caret when focus has no selection", () => {
|
|
const selection = {
|
|
isCollapsed: true,
|
|
anchorNode: null as object | null,
|
|
removeAllRanges() {
|
|
this.anchorNode = null;
|
|
},
|
|
addRange() {
|
|
this.anchorNode = element;
|
|
},
|
|
};
|
|
const documentState = { activeElement: null as object | null };
|
|
let inserted = "";
|
|
const document = {
|
|
get activeElement() {
|
|
return documentState.activeElement;
|
|
},
|
|
getSelection: () => selection,
|
|
createRange: () => ({
|
|
selectNodeContents: () => {},
|
|
collapse: () => {},
|
|
}),
|
|
execCommand(command: string, _showUi: boolean, value: string) {
|
|
if (command !== "insertText") return false;
|
|
inserted = value;
|
|
return true;
|
|
},
|
|
};
|
|
const element = {
|
|
ownerDocument: document,
|
|
focus() {
|
|
documentState.activeElement = element;
|
|
},
|
|
contains(node: object | null) {
|
|
return node === element;
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
insertPlainTextAtComposerSelection(element as unknown as HTMLElement, "LUNA_OK"),
|
|
true
|
|
);
|
|
assert.equal(inserted, "LUNA_OK");
|
|
assert.equal(selection.anchorNode, element);
|
|
});
|
|
|
|
test("keeps OmniRoute DATA_DIR isolation and Docker CDP browser ownership", () => {
|
|
const previousDataDir = process.env.DATA_DIR;
|
|
const previousDedicatedHome = process.env.CODEX_CHATGPT_WEB_HOME;
|
|
const root = mkdtempSync(join(tmpdir(), "omniroute-chatgpt-web-config-"));
|
|
try {
|
|
process.env.DATA_DIR = root;
|
|
delete process.env.CODEX_CHATGPT_WEB_HOME;
|
|
assert.equal(getConfigDir(), join(root, "chatgpt-web-codex"));
|
|
const resolved = resolveBrowserConfig({
|
|
adapter: "chatgpt-web",
|
|
baseUrl: "https://chatgpt.com",
|
|
chatgptWeb: {
|
|
cdpEndpoint: "http://chatgpt-web-codex-browser:9223",
|
|
storageStatePath: join(root, "storage-state.json"),
|
|
},
|
|
});
|
|
assert.equal(resolved.cdpEndpoint, "http://chatgpt-web-codex-browser:9223");
|
|
assert.equal(resolved.chromeExecutablePath, undefined);
|
|
} finally {
|
|
if (previousDataDir === undefined) delete process.env.DATA_DIR;
|
|
else process.env.DATA_DIR = previousDataDir;
|
|
if (previousDedicatedHome === undefined) delete process.env.CODEX_CHATGPT_WEB_HOME;
|
|
else process.env.CODEX_CHATGPT_WEB_HOME = previousDedicatedHome;
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("rejects the previously shipped OmniRoute connector identity after the MCP contract change", () => {
|
|
assert.throws(
|
|
() =>
|
|
resolveBrowserConfig({
|
|
adapter: "chatgpt-web",
|
|
baseUrl: "https://chatgpt.com",
|
|
chatgptWeb: {
|
|
appName: "OmniRoute Codex",
|
|
storageStatePath: "/tmp/omniroute-chatgpt-web-storage-state.json",
|
|
},
|
|
}),
|
|
/newly created connector named "OmniRoute Codex v2"/
|
|
);
|
|
});
|
|
|
|
test("verified capability refresh preserves the credential marker binding", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "omniroute-chatgpt-web-marker-"));
|
|
const statePath = join(root, "storage-state.json");
|
|
const markerPath = loginVerificationMarkerPath(statePath);
|
|
try {
|
|
writeFileSync(statePath, `${JSON.stringify({ cookies: [], origins: [] })}\n`);
|
|
writeFileSync(
|
|
markerPath,
|
|
`${JSON.stringify({
|
|
version: 1,
|
|
authenticated: true,
|
|
verifiedAt: "2026-08-31T00:00:00.000Z",
|
|
cookieFingerprint: "cookie-bound",
|
|
pendingBrowserVerification: true,
|
|
})}\n`
|
|
);
|
|
writeVerificationMarker(statePath, { solAvailable: false, proAvailable: false });
|
|
const marker = JSON.parse(readFileSync(markerPath, "utf8")) as Record<string, unknown>;
|
|
assert.equal(marker.cookieFingerprint, "cookie-bound");
|
|
assert.equal(marker.pendingBrowserVerification, false);
|
|
assert.equal(marker.solAvailable, false);
|
|
assert.equal(marker.proAvailable, false);
|
|
assert.match(String(marker.storageStateFingerprint), /^[a-f0-9]{64}$/);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cookie-header storage state satisfies Playwright cookie requirements", () => {
|
|
const previousDataDir = process.env.DATA_DIR;
|
|
const root = mkdtempSync(join(tmpdir(), "omniroute-chatgpt-web-cookie-state-"));
|
|
try {
|
|
process.env.DATA_DIR = root;
|
|
const statePath = ensureConnectionStorageState(
|
|
"cookie-shape",
|
|
[
|
|
"__Secure-next-auth.session-token.0=first",
|
|
"__Secure-next-auth.session-token.1=second",
|
|
"__Host-next-auth.csrf-token=csrf",
|
|
"oai-did=device",
|
|
].join("; ")
|
|
);
|
|
const state = readConnectionStorageState(statePath);
|
|
const cookies = state.cookies as Array<Record<string, unknown>>;
|
|
|
|
assert.equal(cookies.length, 4);
|
|
assert.equal(
|
|
cookies.every((cookie) => cookie.expires === -1),
|
|
true
|
|
);
|
|
assert.equal(
|
|
cookies.find((cookie) => cookie.name === "__Host-next-auth.csrf-token")?.domain,
|
|
"chatgpt.com"
|
|
);
|
|
assert.equal(cookies.find((cookie) => cookie.name === "oai-did")?.domain, ".chatgpt.com");
|
|
} finally {
|
|
if (previousDataDir === undefined) delete process.env.DATA_DIR;
|
|
else process.env.DATA_DIR = previousDataDir;
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("explicit Responses reasoning effort is read for mismatch preflight", () => {
|
|
assert.equal(reasoningEffortOf({ reasoning: { effort: "high" } }), "high");
|
|
assert.equal(reasoningEffortOf({ reasoning_effort: "xhigh" }), "xhigh");
|
|
});
|
|
|
|
test("Codex detection requires originator or Codex User-Agent", () => {
|
|
assert.equal(isCodexOriginatedHeaders({ originator: "codex_cli_rs" }), true);
|
|
assert.equal(isCodexOriginatedHeaders({ "user-agent": "codex_app/1.0" }), true);
|
|
assert.equal(isCodexOriginatedHeaders({ "user-agent": "openai-node/4" }), false);
|
|
});
|
|
|
|
test("native ChatGPT Web Codex detection also requires thread and turn identity", () => {
|
|
const headers = { originator: "codex_cli_rs" };
|
|
const body = {
|
|
client_metadata: {
|
|
"x-codex-turn-metadata": JSON.stringify({
|
|
thread_id: "thread-1",
|
|
turn_id: "turn-1",
|
|
}),
|
|
},
|
|
};
|
|
assert.equal(hasNativeCodexTurnBinding(body), true);
|
|
assert.equal(isVerifiedNativeCodexRequest(body, headers), true);
|
|
assert.equal(isVerifiedNativeCodexRequest({}, headers), false);
|
|
assert.equal(isVerifiedNativeCodexRequest(body, { "user-agent": "openai-node/4" }), false);
|
|
assert.equal(
|
|
hasNativeCodexTurnBinding({
|
|
client_metadata: {
|
|
"x-codex-turn-metadata": JSON.stringify({ turn_id: "turn-1" }),
|
|
},
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("version two credentials contain storage state but not the raw Cookie", () => {
|
|
const encoded = encodeChatGptWebCodexSecrets({
|
|
storageState: { cookies: [{ name: "session", value: "secret" }], origins: [] },
|
|
runtimeKey: "runtime-secret",
|
|
});
|
|
const parsed = JSON.parse(encoded) as Record<string, unknown>;
|
|
assert.equal(parsed.version, 2);
|
|
assert.equal("cookie" in parsed, false);
|
|
assert.deepEqual(decodeChatGptWebCodexSecrets(encoded).storageState, parsed.storageState);
|
|
});
|
|
|
|
test("legacy Cookie credentials remain decodable for one-time validation", () => {
|
|
assert.equal(
|
|
decodeChatGptWebCodexSecrets(
|
|
JSON.stringify({ version: 1, cookie: "Cookie: session=value", runtimeKey: "key" })
|
|
).cookie,
|
|
"session=value"
|
|
);
|
|
});
|
|
|
|
test("tunnel status is ready only when the process is running and healthy", () => {
|
|
const ready = parseTunnelRuntimeStatus(
|
|
JSON.stringify({ process_running: true, healthy: true, ready: true, runtime_state: "ready" })
|
|
);
|
|
assert.equal(ready.ok, true);
|
|
assert.equal(
|
|
parseTunnelRuntimeStatus(JSON.stringify({ process_running: false, healthy: true, ready: true }))
|
|
.ok,
|
|
false
|
|
);
|
|
});
|
|
|
|
test("tunnel runtime status and stop use only flags accepted by alias commands", () => {
|
|
assert.deepEqual(buildTunnelRuntimeStatusArgs("omniroute-chatgpt-web-codex"), [
|
|
"runtimes",
|
|
"status",
|
|
"omniroute-chatgpt-web-codex",
|
|
"--json",
|
|
]);
|
|
assert.deepEqual(buildTunnelRuntimeStopArgs("omniroute-chatgpt-web-codex"), [
|
|
"runtimes",
|
|
"stop",
|
|
"omniroute-chatgpt-web-codex",
|
|
"--json",
|
|
]);
|
|
});
|
|
|
|
test("tunnel checksum parsing is pinned to the exact release asset", () => {
|
|
const checksum = "a".repeat(64);
|
|
assert.equal(
|
|
parseTunnelChecksum(`${checksum} tunnel-client.zip\n`, "tunnel-client.zip"),
|
|
checksum
|
|
);
|
|
assert.throws(
|
|
() => parseTunnelChecksum(`${checksum} another.zip\n`, "tunnel-client.zip"),
|
|
/no valid entry/
|
|
);
|
|
});
|
|
|
|
test("pins tunnel-client 0.0.13 and upgrades previously shipped builds", () => {
|
|
assert.equal(CHATGPT_WEB_CODEX_TUNNEL_VERSION, "0.0.13");
|
|
assert.equal(tunnelPlatformAsset("darwin", "arm64"), "tunnel-client-v0.0.13-darwin-arm64.zip");
|
|
assert.equal(tunnelClientInstallAction("0.0.13"), "reuse");
|
|
assert.equal(tunnelClientInstallAction("0.0.12"), "upgrade");
|
|
assert.equal(tunnelClientInstallAction("0.0.10"), "upgrade");
|
|
assert.throws(() => tunnelClientInstallAction("0.0.11"), /not a trusted upgrade source/);
|
|
assert.throws(() => tunnelClientInstallAction("9.9.9"), /not a trusted upgrade source/);
|
|
});
|
|
|
|
test("turn broker holds a tool invocation and rejects wrong or duplicate results", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "omniroute-cgw-broker-"));
|
|
const socketPath = join(root, "runtime", "turn-broker.sock");
|
|
const broker = TurnBroker.forSocket(socketPath);
|
|
try {
|
|
const token = await broker.register(
|
|
{
|
|
cwd: root,
|
|
roots: [root],
|
|
writableRoots: [root],
|
|
sandboxPolicy: { type: "dangerFullAccess" },
|
|
tools: [
|
|
{
|
|
name: "exec_command",
|
|
description: "Run a command",
|
|
parameters: { type: "object" },
|
|
},
|
|
],
|
|
},
|
|
10_000
|
|
);
|
|
const claim = await callTurnBroker<{ bindingId: string }>(socketPath, {
|
|
method: "claim",
|
|
token,
|
|
});
|
|
const invocation = callTurnBroker<{ content: unknown[] }>(
|
|
socketPath,
|
|
{
|
|
method: "invoke",
|
|
bindingId: claim.bindingId,
|
|
wireName: "exec_command",
|
|
arguments: { cmd: "pwd" },
|
|
},
|
|
10_000
|
|
);
|
|
const [request] = await broker.nextToolBatch(token);
|
|
assert.ok(request);
|
|
assert.throws(() => broker.completeTool(token, "unknown-call", { content: [] }), /not pending/);
|
|
broker.completeTool(token, request.callId, { content: [{ type: "text", text: root }] });
|
|
assert.deepEqual(await invocation, { content: [{ type: "text", text: root }] });
|
|
assert.throws(() => broker.completeTool(token, request.callId, { content: [] }), /not pending/);
|
|
} finally {
|
|
await broker.close();
|
|
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
});
|
|
|
|
test("revoking a turn rejects a pending connector invocation", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "omniroute-cgw-revoke-"));
|
|
const socketPath = join(root, "runtime", "turn-broker.sock");
|
|
const broker = TurnBroker.forSocket(socketPath);
|
|
try {
|
|
const token = await broker.register(
|
|
{
|
|
cwd: root,
|
|
roots: [root],
|
|
writableRoots: [root],
|
|
sandboxPolicy: { type: "dangerFullAccess" },
|
|
tools: [],
|
|
},
|
|
10_000
|
|
);
|
|
const claim = await callTurnBroker<{ bindingId: string }>(socketPath, {
|
|
method: "claim",
|
|
token,
|
|
});
|
|
const invocation = callTurnBroker(
|
|
socketPath,
|
|
{
|
|
method: "invoke",
|
|
bindingId: claim.bindingId,
|
|
wireName: "exec_command",
|
|
arguments: { cmd: "sleep 30" },
|
|
},
|
|
10_000
|
|
);
|
|
await broker.nextToolBatch(token);
|
|
broker.revoke(token);
|
|
await assert.rejects(invocation, /revoked/);
|
|
} finally {
|
|
await broker.close();
|
|
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
});
|
|
|
|
test("an explicitly bounded turn token expires closed", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "omniroute-cgw-expiry-"));
|
|
const socketPath = join(root, "runtime", "turn-broker.sock");
|
|
const broker = TurnBroker.forSocket(socketPath);
|
|
try {
|
|
const token = await broker.register(
|
|
{
|
|
cwd: root,
|
|
roots: [root],
|
|
writableRoots: [root],
|
|
sandboxPolicy: { type: "dangerFullAccess" },
|
|
tools: [],
|
|
},
|
|
1
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
await assert.rejects(
|
|
callTurnBroker(socketPath, { method: "claim", token }),
|
|
/already finished|turn token is invalid, expired, or revoked/
|
|
);
|
|
} finally {
|
|
await broker.close();
|
|
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
});
|
|
|
|
function requestWithText(text: string): CodexParsedRequest {
|
|
return {
|
|
modelId: "gpt-5.6-sol",
|
|
context: {
|
|
messages: [{ role: "user", content: text, timestamp: 1 }],
|
|
},
|
|
stream: true,
|
|
options: { reasoning: "high" },
|
|
};
|
|
}
|
|
|
|
test("contexts stay inline by default and use explicit transactional multipart transport", () => {
|
|
const capabilities = { localToolsEnabled: false, solAvailable: true, proAvailable: true };
|
|
const small = compileChatGptWebPrompt(requestWithText("hello"), capabilities);
|
|
assert.match(small.text, /<codex_context_json>/);
|
|
assert.equal(small.multipart, undefined);
|
|
|
|
const large = compileChatGptWebPrompt(
|
|
requestWithText("x".repeat(120_001)),
|
|
capabilities,
|
|
undefined,
|
|
{ experimentalMultipartParts: CHATGPT_BIGGER_CONTEXT_PARTS }
|
|
);
|
|
assert.equal(large.multipart?.parts.length, CHATGPT_BIGGER_CONTEXT_PARTS);
|
|
assert.equal(large.text, large.multipart?.commit);
|
|
assert.match(large.multipart?.parts.join("\n") ?? "", /"kind":"message"/);
|
|
assert.doesNotMatch(large.text, /x{1000}/);
|
|
});
|
|
|
|
test("preserves native Codex image inputs as browser attachments", () => {
|
|
const request = requestWithText("inspect the image");
|
|
request.context.messages = [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "inspect the image" },
|
|
{ type: "image", imageUrl: "data:image/png;base64,aW1hZ2U=", detail: "high" },
|
|
],
|
|
timestamp: 1,
|
|
},
|
|
];
|
|
|
|
const compiled = compileChatGptWebPrompt(request, {
|
|
localToolsEnabled: false,
|
|
solAvailable: true,
|
|
proAvailable: true,
|
|
});
|
|
|
|
assert.deepEqual(compiled.images, [
|
|
{
|
|
ref: "codex-input-image-1",
|
|
imageUrl: "data:image/png;base64,aW1hZ2U=",
|
|
detail: "high",
|
|
},
|
|
]);
|
|
assert.match(compiled.text, /"type":"image_attachment"/);
|
|
assert.match(compiled.text, /"attachment_ref":"codex-input-image-1"/);
|
|
});
|
|
|
|
test("preserves Responses input_file bytes as browser attachments", () => {
|
|
const fileBytes = Buffer.from("CHATGPT_WEB_FILE_SENTINEL\n", "utf8");
|
|
const parsed = parseRequest({
|
|
model: "gpt-5.6-sol",
|
|
stream: true,
|
|
reasoning: { effort: "high" },
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [
|
|
{ type: "input_text", text: "Read the attached file" },
|
|
{
|
|
type: "input_file",
|
|
filename: "sentinel.txt",
|
|
file_data: fileBytes.toString("base64"),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const compiled = compileChatGptWebPrompt(parsed, {
|
|
localToolsEnabled: false,
|
|
solAvailable: true,
|
|
proAvailable: true,
|
|
});
|
|
|
|
assert.deepEqual(compiled.files, [
|
|
{
|
|
ref: "codex-input-file-1",
|
|
filename: "sentinel.txt",
|
|
fileData: fileBytes.toString("base64"),
|
|
},
|
|
]);
|
|
assert.match(compiled.text, /"type":"file_attachment"/);
|
|
assert.match(compiled.text, /"attachment_ref":"codex-input-file-1"/);
|
|
const payloads = chatGptPromptFilePayloads(compiled);
|
|
assert.equal(payloads.length, 1);
|
|
assert.equal(payloads[0]?.name, "sentinel.txt");
|
|
assert.equal(payloads[0]?.mimeType, "text/plain");
|
|
assert.equal(payloads[0]?.buffer.toString("utf8"), "CHATGPT_WEB_FILE_SENTINEL\n");
|
|
});
|
|
|
|
test("rejects unresolved Responses file_id references instead of fabricating file text", () => {
|
|
assert.throws(
|
|
() =>
|
|
parseRequest({
|
|
model: "gpt-5.6-sol",
|
|
stream: true,
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_file", file_id: "file-unavailable" }],
|
|
},
|
|
],
|
|
}),
|
|
/cannot resolve input_file file_id/i
|
|
);
|
|
});
|
|
|
|
test("rejects unresolved and remote Responses image references before browser dispatch", () => {
|
|
const requestWith = (image: Record<string, unknown>) => ({
|
|
model: "gpt-5.6-sol",
|
|
stream: true,
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_image", ...image }],
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.throws(
|
|
() => parseRequest(requestWith({ file_id: "file-unavailable" })),
|
|
/cannot resolve input_image file_id/i
|
|
);
|
|
assert.throws(
|
|
() => parseRequest(requestWith({ image_url: "https:\/\/example.com\/remote.png" })),
|
|
/supports inline data URLs only/i
|
|
);
|
|
});
|
|
|
|
test("accepts inline input_file data URLs and rejects remote file URLs", () => {
|
|
const parsed = parseRequest({
|
|
model: "gpt-5.6-sol",
|
|
stream: true,
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "input_file",
|
|
filename: "../probe.csv",
|
|
file_url: "data:text/csv;base64,Y29sdW1uCg==",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
const compiled = compileChatGptWebPrompt(parsed, {
|
|
localToolsEnabled: false,
|
|
solAvailable: true,
|
|
proAvailable: true,
|
|
});
|
|
const [payload] = chatGptPromptFilePayloads(compiled);
|
|
assert.equal(payload?.name, "probe.csv");
|
|
assert.equal(payload?.mimeType, "text/csv");
|
|
assert.equal(payload?.buffer.toString("utf8"), "column\n");
|
|
|
|
assert.throws(
|
|
() =>
|
|
parseRequest({
|
|
model: "gpt-5.6-sol",
|
|
stream: true,
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "input_file",
|
|
filename: "remote.pdf",
|
|
file_url: "https://example.com/remote.pdf",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
/supports inline data URLs only/i
|
|
);
|
|
});
|
|
|
|
test("session registry reports waiting turns as settled retained sessions", async () => {
|
|
const sessions = new ChatGptTurnSessions();
|
|
assert.equal(sessions.activeCount(), 0);
|
|
assert.equal(sessions.waitingCount(), 0);
|
|
|
|
let resolveBrowser: (answer: string) => void = () => {};
|
|
const browser = new Promise<string>((resolve) => {
|
|
resolveBrowser = resolve;
|
|
});
|
|
sessions.getOrCreate("turn-a", () => ({
|
|
mode: "read-only",
|
|
browser,
|
|
physicalSettlement: browser.then(() => undefined),
|
|
trace: new ChatGptTraceFeed(),
|
|
text: new ChatGptTextFeed(),
|
|
cancel() {},
|
|
}));
|
|
assert.equal(sessions.activeCount(), 1);
|
|
assert.equal(sessions.waitingCount(), 0);
|
|
|
|
resolveBrowser("done");
|
|
await sessions.find("turn-a")?.browserOutcome;
|
|
assert.equal(sessions.activeCount(), 0);
|
|
assert.equal(sessions.waitingCount(), 1);
|
|
});
|
|
|
|
test("forced previous_response_id state flushes immediately and reloads after an isolate miss", () => {
|
|
const home = mkdtempSync(join(tmpdir(), "chatgpt-web-codex-state-"));
|
|
const previousHome = process.env.CODEX_CHATGPT_WEB_HOME;
|
|
process.env.CODEX_CHATGPT_WEB_HOME = home;
|
|
try {
|
|
resetResponseStateForTests();
|
|
const namespace = "conn:thread_live:turn_live";
|
|
rememberResponseState(
|
|
{
|
|
store: false,
|
|
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "pwd" }] }],
|
|
},
|
|
{
|
|
id: "resp_force_flush",
|
|
status: "completed",
|
|
output: [{ type: "function_call", call_id: "call_1", name: "exec_command" }],
|
|
},
|
|
{ force: true, namespace }
|
|
);
|
|
const snapshot = join(home, "responses-state.json");
|
|
assert.equal(existsSync(snapshot), true);
|
|
resetResponseStateForTests();
|
|
const continuation = {
|
|
previous_response_id: "resp_force_flush",
|
|
input: [
|
|
{ type: "function_call_output", call_id: "call_1", output: "/Users/backryun/OmniRoute" },
|
|
],
|
|
};
|
|
const expanded = expandPreviousResponseInput(continuation, namespace);
|
|
assert.notEqual(expanded, continuation);
|
|
assert.ok(Array.isArray((expanded as { input: unknown[] }).input));
|
|
assert.equal((expanded as { input: unknown[] }).input.length, 3);
|
|
const foreign = expandPreviousResponseInput(continuation, "other-turn");
|
|
assert.equal(foreign, continuation);
|
|
} finally {
|
|
resetResponseStateForTests();
|
|
if (previousHome === undefined) delete process.env.CODEX_CHATGPT_WEB_HOME;
|
|
else process.env.CODEX_CHATGPT_WEB_HOME = previousHome;
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("concurrent response-state writers merge their snapshots instead of overwriting", async () => {
|
|
const home = mkdtempSync(join(tmpdir(), "chatgpt-web-codex-state-merge-"));
|
|
const previousHome = process.env.CODEX_CHATGPT_WEB_HOME;
|
|
process.env.CODEX_CHATGPT_WEB_HOME = home;
|
|
const nonce = `${process.pid}-${Date.now()}`;
|
|
const writerA = await import(
|
|
`../../open-sse/vendor/codex-chatgpt-web/responses/state.ts?writer-a=${nonce}`
|
|
);
|
|
const writerB = await import(
|
|
`../../open-sse/vendor/codex-chatgpt-web/responses/state.ts?writer-b=${nonce}`
|
|
);
|
|
try {
|
|
assert.notEqual(writerA, writerB);
|
|
writerA.resetResponseStateForTests();
|
|
writerB.resetResponseStateForTests();
|
|
writerA.rememberResponseState(
|
|
{ store: false, input: "request-a" },
|
|
{ id: "resp_writer_a", status: "completed", output: [{ type: "output_text", text: "a" }] },
|
|
{ force: true, namespace: "namespace-a" }
|
|
);
|
|
writerB.rememberResponseState(
|
|
{ store: false, input: "request-b" },
|
|
{ id: "resp_writer_b", status: "completed", output: [{ type: "output_text", text: "b" }] },
|
|
{ force: true, namespace: "namespace-b" }
|
|
);
|
|
|
|
writerA.resetResponseStateForTests();
|
|
writerB.resetResponseStateForTests();
|
|
const continuationA = { previous_response_id: "resp_writer_a", input: "continue-a" };
|
|
const continuationB = { previous_response_id: "resp_writer_b", input: "continue-b" };
|
|
assert.notEqual(
|
|
writerA.expandPreviousResponseInput(continuationA, "namespace-a"),
|
|
continuationA
|
|
);
|
|
assert.notEqual(
|
|
writerB.expandPreviousResponseInput(continuationB, "namespace-b"),
|
|
continuationB
|
|
);
|
|
} finally {
|
|
writerA.resetResponseStateForTests();
|
|
writerB.resetResponseStateForTests();
|
|
if (previousHome === undefined) delete process.env.CODEX_CHATGPT_WEB_HOME;
|
|
else process.env.CODEX_CHATGPT_WEB_HOME = previousHome;
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("large attachment response state survives a separate isolate", async () => {
|
|
const home = mkdtempSync(join(tmpdir(), "chatgpt-web-codex-state-large-"));
|
|
const previousHome = process.env.CODEX_CHATGPT_WEB_HOME;
|
|
process.env.CODEX_CHATGPT_WEB_HOME = home;
|
|
const nonce = `${process.pid}-${Date.now()}`;
|
|
const writer = await import(
|
|
`../../open-sse/vendor/codex-chatgpt-web/responses/state.ts?large-writer=${nonce}`
|
|
);
|
|
const reader = await import(
|
|
`../../open-sse/vendor/codex-chatgpt-web/responses/state.ts?large-reader=${nonce}`
|
|
);
|
|
try {
|
|
assert.notEqual(writer, reader);
|
|
writer.resetResponseStateForTests();
|
|
reader.resetResponseStateForTests();
|
|
const fileData = "a".repeat(2_200_000);
|
|
writer.rememberResponseState(
|
|
{
|
|
store: false,
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_file", filename: "large.txt", file_data: fileData }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "resp_large_attachment",
|
|
status: "completed",
|
|
output: [{ type: "output_text", text: "received" }],
|
|
},
|
|
{ force: true, namespace: "namespace-large" }
|
|
);
|
|
assert.equal(existsSync(join(home, "responses-state-large")), true);
|
|
|
|
const continuation = {
|
|
previous_response_id: "resp_large_attachment",
|
|
input: "continue-large",
|
|
};
|
|
const expanded = reader.expandPreviousResponseInput(continuation, "namespace-large");
|
|
assert.notEqual(expanded, continuation);
|
|
assert.equal((expanded as { input: unknown[] }).input.length, 3);
|
|
} finally {
|
|
writer.resetResponseStateForTests();
|
|
reader.resetResponseStateForTests();
|
|
if (previousHome === undefined) delete process.env.CODEX_CHATGPT_WEB_HOME;
|
|
else process.env.CODEX_CHATGPT_WEB_HOME = previousHome;
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("namespaced continuations reject legacy state without a namespace", () => {
|
|
const home = mkdtempSync(join(tmpdir(), "chatgpt-web-codex-state-namespace-"));
|
|
const previousHome = process.env.CODEX_CHATGPT_WEB_HOME;
|
|
process.env.CODEX_CHATGPT_WEB_HOME = home;
|
|
try {
|
|
resetResponseStateForTests();
|
|
rememberResponseState(
|
|
{ store: false, input: "private-history" },
|
|
{ id: "resp_legacy_namespace", status: "completed", output: [] },
|
|
{ force: true }
|
|
);
|
|
const continuation = { previous_response_id: "resp_legacy_namespace", input: "foreign" };
|
|
assert.equal(expandPreviousResponseInput(continuation, "different-namespace"), continuation);
|
|
} finally {
|
|
resetResponseStateForTests();
|
|
if (previousHome === undefined) delete process.env.CODEX_CHATGPT_WEB_HOME;
|
|
else process.env.CODEX_CHATGPT_WEB_HOME = previousHome;
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("self-contained Codex continuations ignore an unknown previous_response_id instead of 409ing", () => {
|
|
resetResponseStateForTests();
|
|
const body = {
|
|
previous_response_id: "resp_missing_from_this_isolate",
|
|
input: [
|
|
{ type: "message", role: "user", content: [{ type: "input_text", text: "pwd" }] },
|
|
{
|
|
type: "function_call",
|
|
call_id: "call_1",
|
|
name: "exec_command",
|
|
arguments: '{"cmd":"pwd"}',
|
|
},
|
|
{ type: "function_call_output", call_id: "call_1", output: "/Users/backryun/OmniRoute" },
|
|
],
|
|
};
|
|
assert.equal(inputHasSelfContainedCodexContinuation(body), true);
|
|
const resolved = resolveChatGptWebCodexPreviousResponse(body, "conn:thread:turn");
|
|
assert.equal(resolved.ok, true);
|
|
assert.equal(resolved.body.previous_response_id, undefined);
|
|
assert.deepEqual(resolved.body.input, body.input);
|
|
|
|
const naked = {
|
|
previous_response_id: "resp_missing_from_this_isolate",
|
|
input: [{ type: "function_call_output", call_id: "call_1", output: "/tmp" }],
|
|
};
|
|
assert.equal(inputHasSelfContainedCodexContinuation(naked), false);
|
|
assert.equal(resolveChatGptWebCodexPreviousResponse(naked, "conn:thread:turn").ok, false);
|
|
});
|
|
|
|
test("a previous_response_id binding miss does not cool down the ChatGPT Web Codex connection", () => {
|
|
const result = checkFallbackError(
|
|
409,
|
|
"[chatgpt-web-codex/instant] previous_response_id does not belong to this verified Codex turn",
|
|
0,
|
|
"instant",
|
|
"chatgpt-web-codex",
|
|
null,
|
|
null,
|
|
{ code: "invalid_previous_response_binding" }
|
|
);
|
|
assert.equal(result.shouldFallback, false);
|
|
assert.equal(result.cooldownMs, 0);
|
|
assert.equal(result.skipProviderBreaker, true);
|
|
});
|