Files
OmniRoute/tests/unit/mcp-stdio-json-purity.test.ts
Armin Anton” ∴ 8f390efffd feat(codex): self-contained codex app-server transport (executor + provider + sign-in) (#11205)
Merged after conflict resolution: the 5 conflicting test files were the base-red drains that #11201 already landed on the tip — kept the tip versions; the feature content is untouched. Validated on the combined batch board + this branch: codex-app-server + codex-gpt56-catalog 25/25, typecheck:core clean, docs-counts green (351 providers), provider-consistency 268/351/0. The opt-in codex-app-server transport (JSON-RPC-over-WS, turn/completed-awaited close, Responses SSE bridge) leaves the default codex path untouched. Thank you @arminanton — a 3.4k-line transport with the docs wave and tests to match!
2026-08-23 10:20:06 -03:00

96 lines
4.3 KiB
TypeScript

// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
// better-sqlite3 is a native addon; production and CI load it normally, but some
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
// would pollute) fails HERE while passing in CI. This is a known environment
// limitation, not a defect in the code under test: the OmniRoute runtime itself
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { join } from "node:path";
const ROOT = new URL("../..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
/**
* Regression coverage: `omniroute --mcp` (the stdio transport Claude Desktop and other MCP
* clients spawn) must write nothing but JSON-RPC to stdout. DB init — a side effect of
* `createMcpServer()`'s tool registration reading compression settings — used to log via
* plain `console.log` before any redirect was in place (ES module static imports are hoisted
* and evaluate before any code inside the importing module's own functions runs, so a
* redirect placed inside server.ts itself was too late). That leaked lines like
* "[DB] Changing cache_size from 65536KB to 16384KB" straight onto stdout, corrupting the
* JSON-RPC stream client-side (e.g. Claude Desktop: `Unexpected token 'D', "[DB] Changi"...
* is not valid JSON`). Fixed by preloading bin/mcpStdioConsoleGuard.mjs via `node --import`
* (bin/mcp-server.mjs) — the only point early enough to run before the MCP entry's module
* graph evaluates at all.
*/
describe("omniroute --mcp stdio transport", () => {
it("writes only valid JSON-RPC to stdout — no DB init or other startup logging leaks through", async () => {
const child = spawn(process.execPath, [join(ROOT, "bin", "omniroute.mjs"), "--mcp"], {
cwd: ROOT,
env: process.env,
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.stdin.write(
`${JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "regression-test", version: "0" },
},
})}\n`
);
// The full chain (omniroute.mjs CLI startup + spawned MCP child, each paying a tsx
// import + the child's DB init/migrations) takes ~10s on a warm dev box and longer on
// loaded CI runners — a fixed 4s sleep made this test red from birth. Poll for the
// first stdout line instead, then give the stream a short settle window so any
// late startup logging that WOULD corrupt the protocol still gets caught.
const deadline = Date.now() + 60_000;
while (!stdout.includes("\n") && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
await new Promise((resolve) => setTimeout(resolve, 1000));
child.kill();
const stdoutLines = stdout.split("\n").filter((line) => line.trim().length > 0);
assert.ok(
stdoutLines.length > 0,
"expected at least one line on stdout (the initialize response)"
);
for (const line of stdoutLines) {
assert.doesNotThrow(
() => JSON.parse(line),
`stdout line is not valid JSON (startup logging leaked onto stdout): ${line.slice(0, 120)}`
);
}
const initResponse = stdoutLines.map((line) => JSON.parse(line)).find((msg) => msg.id === 1);
assert.ok(initResponse, "expected an initialize response with id 1 on stdout");
assert.equal(initResponse.jsonrpc, "2.0");
// The DB init logging must still happen — just on stderr, not stdout.
assert.ok(
stderr.includes("[DB]"),
"expected DB init logging on stderr (proves it was redirected, not silently dropped)"
);
});
});