From 421c9f6ff8e7a07f978ce0f3b5d86ced5be5b4a1 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Tue, 28 Jul 2026 17:00:55 +0000 Subject: [PATCH 1/2] test(codex): gate the 0.145 CLI upgrade with JSONL replay --- tests/fixtures/codex/README.md | 36 +++++++++++ tests/fixtures/codex/exec-0.141.0.jsonl | 4 ++ tests/fixtures/codex/exec-0.145.0.jsonl | 8 +++ tests/unit/backends/codex-jsonlParser.test.ts | 63 +++++++++++++++++++ tests/unit/backends/codex.test.ts | 50 +++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 tests/fixtures/codex/README.md create mode 100644 tests/fixtures/codex/exec-0.141.0.jsonl create mode 100644 tests/fixtures/codex/exec-0.145.0.jsonl diff --git a/tests/fixtures/codex/README.md b/tests/fixtures/codex/README.md new file mode 100644 index 000000000..f967c7405 --- /dev/null +++ b/tests/fixtures/codex/README.md @@ -0,0 +1,36 @@ +# Codex CLI JSONL fixtures + +These sanitized fixtures are recordings from `codex exec --json` used to gate +worker CLI upgrades. Thread IDs are replaced with stable fixture values; event +shapes and usage counters are unchanged. + +Recordings for MNG-1753 were captured on 2026-07-28: + +- `exec-0.141.0.jsonl`: `@openai/codex@0.141.0`, model `gpt-5.4`, one turn. +- `exec-0.145.0.jsonl`: `@openai/codex@0.145.0`, model `gpt-5.6-sol`, followed + by `codex exec resume` for a second turn. + +The 0.145.0 recording confirms that `turn.completed.usage` remains cumulative: +input/output totals advance from `15428/6` to `30875/13`. The engine replay +test therefore expects the second persisted delta to be `15447/7`. + +For future upgrades, record a minimal first turn and resumed turn with both the +old and target pins, sanitize identifiers and prompt content, then replay the +fixtures through: + +```bash +npx vitest run --project unit-backends \ + tests/unit/backends/codex-jsonlParser.test.ts \ + tests/unit/backends/codex.test.ts +``` + +The engine replay must produce no +`Unrecognized Codex event type — no fields extracted` log entries and no +`Codex turn.completed reported lower cumulative usage` warnings. + +The `write_stdin failed: stdin is closed` detector must remain independently +pinned in `codex.test.ts`. As of this recording, upstream issue +openai/codex#18578 is still open and its stderr examples retain the +`codex_core::tools::router` module path. Upstream PR openai/codex#28895 was +merged, but it hardens retry behavior for remote exec-server writes rather than +the non-TTY process path described in #18578. diff --git a/tests/fixtures/codex/exec-0.141.0.jsonl b/tests/fixtures/codex/exec-0.141.0.jsonl new file mode 100644 index 000000000..fc106f3e5 --- /dev/null +++ b/tests/fixtures/codex/exec-0.141.0.jsonl @@ -0,0 +1,4 @@ +{"type":"thread.started","thread_id":"fixture-thread-0.141.0"} +{"type":"turn.started"} +{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"fixture-ok"}} +{"type":"turn.completed","usage":{"input_tokens":10859,"cached_input_tokens":2432,"output_tokens":16,"reasoning_output_tokens":8}} diff --git a/tests/fixtures/codex/exec-0.145.0.jsonl b/tests/fixtures/codex/exec-0.145.0.jsonl new file mode 100644 index 000000000..6417e89ae --- /dev/null +++ b/tests/fixtures/codex/exec-0.145.0.jsonl @@ -0,0 +1,8 @@ +{"type":"thread.started","thread_id":"fixture-thread-0.145.0"} +{"type":"turn.started"} +{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"fixture-ok"}} +{"type":"turn.completed","usage":{"input_tokens":15428,"cached_input_tokens":0,"cache_write_input_tokens":0,"output_tokens":6,"reasoning_output_tokens":0}} +{"type":"thread.started","thread_id":"fixture-thread-0.145.0"} +{"type":"turn.started"} +{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"fixture-resumed"}} +{"type":"turn.completed","usage":{"input_tokens":30875,"cached_input_tokens":15104,"cache_write_input_tokens":0,"output_tokens":13,"reasoning_output_tokens":0}} diff --git a/tests/unit/backends/codex-jsonlParser.test.ts b/tests/unit/backends/codex-jsonlParser.test.ts index 53b1e37b9..f4319d88a 100644 --- a/tests/unit/backends/codex-jsonlParser.test.ts +++ b/tests/unit/backends/codex-jsonlParser.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { @@ -9,6 +10,18 @@ import { resolveUsageRecord, } from '../../../src/backends/codex/jsonlParser.js'; +const RECORDED_EXEC_FIXTURES = ['0.141.0', '0.145.0'] as const; + +function readRecordedExecFixture(version: (typeof RECORDED_EXEC_FIXTURES)[number]) { + return readFileSync( + new URL(`../../fixtures/codex/exec-${version}.jsonl`, import.meta.url), + 'utf8', + ) + .trim() + .split('\n') + .map((line) => JSON.parse(line) as Record); +} + // ─── extractTextFromContentParts ──────────────────────────────────────────── describe('extractTextFromContentParts', () => { @@ -272,6 +285,56 @@ describe('resolveUsageRecord', () => { // ─── parseCodexEvent ───────────────────────────────────────────────────────── describe('parseCodexEvent', () => { + it.each( + RECORDED_EXEC_FIXTURES, + )('replays the Codex CLI %s exec stream without parser drift', (version) => { + const events = readRecordedExecFixture(version); + const parsed = events.map(parseCodexEvent); + + expect(events.map((event) => event.type)).toEqual([ + 'thread.started', + 'turn.started', + 'item.completed', + 'turn.completed', + ...(version === '0.145.0' + ? ['thread.started', 'turn.started', 'item.completed', 'turn.completed'] + : []), + ]); + expect(parsed[2]).toMatchObject({ + textParts: ['fixture-ok'], + toolCall: null, + usage: null, + }); + expect(parsed[3].usage).toEqual( + version === '0.141.0' + ? { + inputTokens: 10859, + outputTokens: 16, + cachedTokens: 2432, + reasoningTokens: 8, + } + : { + inputTokens: 15428, + outputTokens: 6, + cachedTokens: 0, + reasoningTokens: 0, + }, + ); + if (version === '0.145.0') { + expect(parsed[6]).toMatchObject({ + textParts: ['fixture-resumed'], + toolCall: null, + usage: null, + }); + expect(parsed[7].usage).toEqual({ + inputTokens: 30875, + outputTokens: 13, + cachedTokens: 15104, + reasoningTokens: 0, + }); + } + }); + it('parses a complete text event correctly', () => { const result = parseCodexEvent({ type: 'text', text: 'Hello, world!' }); expect(result.textParts).toContain('Hello, world!'); diff --git a/tests/unit/backends/codex.test.ts b/tests/unit/backends/codex.test.ts index 547092f75..9e1d662a8 100644 --- a/tests/unit/backends/codex.test.ts +++ b/tests/unit/backends/codex.test.ts @@ -56,6 +56,17 @@ import { } from '../../../src/backends/codex/settings.js'; import type { AgentExecutionPlan } from '../../../src/backends/types.js'; +const RECORDED_EXEC_FIXTURES = ['0.141.0', '0.145.0'] as const; + +function recordedExecLines(version: (typeof RECORDED_EXEC_FIXTURES)[number]): string[] { + return readFileSync( + new URL(`../../fixtures/codex/exec-${version}.jsonl`, import.meta.url), + 'utf8', + ) + .trim() + .split('\n'); +} + function makeInput(overrides: Partial = {}): AgentExecutionPlan { return { agentType: 'implementation', @@ -672,6 +683,45 @@ describe('CodexEngine', () => { ); }); + it.each( + RECORDED_EXEC_FIXTURES, + )('replays the Codex CLI %s exec stream without unrecognized events', async (version) => { + mockSpawn.mockImplementation((_cmd: string, args: string[]) => { + const outputPath = args[args.indexOf('-o') + 1]; + return createMockChild({ + stdoutLines: recordedExecLines(version), + onBeforeClose: () => writeFileSync(outputPath, 'fixture-ok', 'utf-8'), + }); + }); + + const engine = new CodexEngine(); + const input = makeInput({ repoDir: workspaceDir, runId: `fixture-${version}` }); + const result = await engine.execute(input); + + expect(result.success).toBe(true); + expect(input.progressReporter.onText).toHaveBeenCalledWith('fixture-ok'); + expect(input.logWriter).not.toHaveBeenCalledWith( + 'DEBUG', + 'Unrecognized Codex event type — no fields extracted', + expect.anything(), + ); + expect(mockStoreLlmCall).toHaveBeenCalledWith( + expect.objectContaining({ + inputTokens: version === '0.141.0' ? 10859 : 15428, + outputTokens: version === '0.141.0' ? 16 : 6, + }), + ); + if (version === '0.145.0') { + expect(mockStoreLlmCall).toHaveBeenLastCalledWith( + expect.objectContaining({ + inputTokens: 15447, + outputTokens: 7, + cachedTokens: 15104, + }), + ); + } + }); + it('logs full event payload including item and delta on unrecognized events', async () => { const unknownEvent = { type: 'some.future.event', From b566323f9b488659f06de832f52d19b8b35b10d9 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Tue, 28 Jul 2026 17:14:15 +0000 Subject: [PATCH 2/2] fix(deps): resolve high-severity prod audit failures (axios, js-yaml, brace-expansion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `npm audit --omit=dev --audit-level=high` CI step failed on three newly published high-severity advisories in production dependencies: - axios (<1.18.0): multiple DoS / prototype-pollution advisories → bump override to ^1.18.0 - js-yaml (<4.3.0): YAML merge-key quadratic CPU DoS → bump direct dep to ^4.3.0 - brace-expansion (<=5.0.7): unbounded expansion OOM DoS → bump override to ^5.0.8. brace-expansion 5.x drops the default ESM export that minimatch@9 imports, so also override minimatch to ^10.0.3 (uses the named `expand` import) to keep the tree consistent. Full unit suite (10,942 tests), typecheck, lint, build, and prod audit all pass locally. Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 153 ++++++++++++++++------------------------------ package.json | 7 ++- 2 files changed, 55 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index bcdd6df77..3367f740d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "fastest-levenshtein": "^1.0.16", "hono": "^4.12.25", "jira.js": "^5.3.0", - "js-yaml": "^4.2.0", + "js-yaml": "^4.3.0", "llmist": "^16.0.4", "marklassian": "^1.1.0", "open": "^11.0.0", @@ -3853,21 +3853,6 @@ "version": "10.4.3", "license": "ISC" }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/archiver-utils/node_modules/path-scurry": { "version": "1.11.1", "license": "BlueOak-1.0.0", @@ -4009,16 +3994,42 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/b4a": { "version": "1.7.3", "license": "Apache-2.0", @@ -4032,8 +4043,13 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/bare-events": { "version": "2.8.2", @@ -4119,12 +4135,15 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/buffer": { @@ -6131,18 +6150,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -6953,9 +6960,9 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -7316,21 +7323,6 @@ "node": ">=22.0.0" } }, - "node_modules/llmist/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/llmist/node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", @@ -7580,12 +7572,12 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -8402,18 +8394,6 @@ "minimatch": "^5.1.0" } }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -8541,21 +8521,6 @@ "version": "10.4.3", "license": "ISC" }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rimraf/node_modules/path-scurry": { "version": "1.11.1", "license": "BlueOak-1.0.0", @@ -9039,22 +9004,6 @@ "dev": true, "license": "ISC" }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/test-exclude/node_modules/path-scurry": { "version": "1.11.1", "dev": true, diff --git a/package.json b/package.json index 3aa84cd0c..b521755cb 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "fastest-levenshtein": "^1.0.16", "hono": "^4.12.25", "jira.js": "^5.3.0", - "js-yaml": "^4.2.0", + "js-yaml": "^4.3.0", "llmist": "^16.0.4", "marklassian": "^1.1.0", "open": "^11.0.0", @@ -138,8 +138,9 @@ "overrides": { "lodash": "^4.18.1", "lodash-es": "^4.18.1", - "brace-expansion": "^2.0.3", - "axios": "^1.15.0", + "brace-expansion": "^5.0.8", + "minimatch": "^10.0.3", + "axios": "^1.18.0", "protobufjs": "^7.6.4", "form-data": "^4.0.6" }