From f0732f48ab33ad522f07ee1a9c808b64d8daa0a3 Mon Sep 17 00:00:00 2001 From: Abdulrahman Alfozan Date: Sun, 7 Jun 2026 22:27:43 -0700 Subject: [PATCH 1/2] Fix Codex image arg order - Put the Codex exec subcommand before provider extra args so image flags belong to codex exec.\n- Add regression coverage for Codex image args so -i cannot consume exec.\n\nFixes #242. --- src/llm/cli.ts | 2 +- tests/llm.cli.regressions.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/llm/cli.ts b/src/llm/cli.ts index deee9647f..073d314ee 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -304,8 +304,8 @@ export async function runCliModel({ if (isolatedCodexHome) { await copyCodexAuthFiles(effectiveEnv.CODEX_HOME, isolatedCodexHome); } - args.push(...codexExtraArgs); args.push("exec"); + args.push(...codexExtraArgs); if (shouldIsolateCodex) { args.push("--ephemeral", "--ignore-user-config", "--ignore-rules"); if (isolatedCwd) args.push("-C", isolatedCwd); diff --git a/tests/llm.cli.regressions.test.ts b/tests/llm.cli.regressions.test.ts index dec519dca..bbb19227e 100644 --- a/tests/llm.cli.regressions.test.ts +++ b/tests/llm.cli.regressions.test.ts @@ -99,6 +99,33 @@ describe("runCliModel regressions", () => { expect(result.text).toBe("Hello world"); }); + it("passes Codex image args after exec so -i does not consume the subcommand", async () => { + let seenArgs: string[] = []; + + const result = await runCliModel({ + provider: "codex", + prompt: "summarize this image", + model: "gpt-5.2", + allowTools: true, + timeoutMs: 1000, + env: {}, + config: null, + extraArgs: ["-i", "/tmp/image.jpg"], + execFileImpl: (_cmd, args, _opts, cb) => { + seenArgs = [...args]; + const outputIndex = args.indexOf("--output-last-message"); + const outputPath = outputIndex >= 0 ? args[outputIndex + 1] : null; + if (!outputPath) throw new Error("missing output path"); + writeFileSync(outputPath, "ok", "utf8"); + cb(null, "", ""); + return { stdin: { write() {}, end() {} } } as unknown as ChildProcess; + }, + }); + + expect(result.text).toBe("ok"); + expect(seenArgs.indexOf("exec")).toBeLessThan(seenArgs.indexOf("-i")); + }); + it("codex does not leak lifecycle JSONL when no assistant text was produced", async () => { await expect( runCliModel({ From d21f79a8b8c56300e18a183bd614f0e38d0d45b1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2026 00:02:03 +0100 Subject: [PATCH 2/2] test: strengthen Codex exec argv coverage --- CHANGELOG.md | 1 + src/llm/cli.ts | 3 +- tests/llm.cli.regressions.test.ts | 76 +++++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 841b4239c..6b1484cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- CLI: pass Codex image attachments to `codex exec` so local image summaries no longer fail before starting (#242, #243, thanks @alfozan). - Chrome extension: abort stale side-panel summary streams on tab changes so delayed output from a closed or replaced tab cannot render under the new page title. - Core: extract video IDs from YouTube `/live/` URLs so live and premiere links no longer abort summarization (#232, thanks @devYRPauli). - Chrome extension: keep YouTube slide cards on the shared slide-summary path so local browser thumbnails receive the same summary text shape as CLI `--slides`. diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 073d314ee..d16a388dc 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -304,8 +304,7 @@ export async function runCliModel({ if (isolatedCodexHome) { await copyCodexAuthFiles(effectiveEnv.CODEX_HOME, isolatedCodexHome); } - args.push("exec"); - args.push(...codexExtraArgs); + args.push("exec", ...codexExtraArgs); if (shouldIsolateCodex) { args.push("--ephemeral", "--ignore-user-config", "--ignore-rules"); if (isolatedCwd) args.push("-C", isolatedCwd); diff --git a/tests/llm.cli.regressions.test.ts b/tests/llm.cli.regressions.test.ts index bbb19227e..b25689555 100644 --- a/tests/llm.cli.regressions.test.ts +++ b/tests/llm.cli.regressions.test.ts @@ -101,16 +101,67 @@ describe("runCliModel regressions", () => { it("passes Codex image args after exec so -i does not consume the subcommand", async () => { let seenArgs: string[] = []; + let stdinText = ""; + const prompt = "summarize this image"; const result = await runCliModel({ provider: "codex", - prompt: "summarize this image", + prompt, model: "gpt-5.2", allowTools: true, timeoutMs: 1000, env: {}, config: null, - extraArgs: ["-i", "/tmp/image.jpg"], + extraArgs: ["-i", "/tmp/image-1.jpg", "/tmp/image-2.jpg"], + execFileImpl: (_cmd, args, _opts, cb) => { + seenArgs = [...args]; + const outputIndex = args.indexOf("--output-last-message"); + const outputPath = outputIndex >= 0 ? args[outputIndex + 1] : null; + if (!outputPath) throw new Error("missing output path"); + writeFileSync(outputPath, "ok", "utf8"); + cb(null, "", ""); + return { + stdin: { + write(value: string) { + stdinText += value; + }, + end() {}, + }, + } as unknown as ChildProcess; + }, + }); + + const execIndex = seenArgs.indexOf("exec"); + const imageFlagIndex = seenArgs.indexOf("-i"); + const firstImageIndex = seenArgs.indexOf("/tmp/image-1.jpg"); + const secondImageIndex = seenArgs.indexOf("/tmp/image-2.jpg"); + const outputIndex = seenArgs.indexOf("--output-last-message"); + const modelIndex = seenArgs.indexOf("-m"); + const verbosityIndex = seenArgs.indexOf("-c"); + + expect(result.text).toBe("ok"); + expect(execIndex).toBe(0); + expect(imageFlagIndex).toBeGreaterThan(execIndex); + expect(firstImageIndex).toBe(imageFlagIndex + 1); + expect(secondImageIndex).toBe(firstImageIndex + 1); + expect(outputIndex).toBeGreaterThan(secondImageIndex); + expect(modelIndex).toBeGreaterThan(execIndex); + expect(verbosityIndex).toBeGreaterThan(execIndex); + expect(seenArgs).not.toContain(prompt); + expect(stdinText).toBe(prompt); + }); + + it("keeps generated Codex flags under exec for non-image summaries", async () => { + let seenArgs: string[] = []; + + const result = await runCliModel({ + provider: "codex", + prompt: "summarize text", + model: "gpt-fast", + allowTools: false, + timeoutMs: 1000, + env: {}, + config: { codex: { extraArgs: ["--sandbox", "read-only"] } }, execFileImpl: (_cmd, args, _opts, cb) => { seenArgs = [...args]; const outputIndex = args.indexOf("--output-last-message"); @@ -122,8 +173,27 @@ describe("runCliModel regressions", () => { }, }); + const execIndex = seenArgs.indexOf("exec"); + expect(result.text).toBe("ok"); - expect(seenArgs.indexOf("exec")).toBeLessThan(seenArgs.indexOf("-i")); + expect(execIndex).toBe(0); + expect(seenArgs).not.toContain("-i"); + for (const flag of [ + "--sandbox", + "--ephemeral", + "--ignore-user-config", + "--ignore-rules", + "-C", + "--output-last-message", + "--skip-git-repo-check", + "--json", + "-m", + "-c", + ]) { + expect(seenArgs.indexOf(flag)).toBeGreaterThan(execIndex); + } + expect(seenArgs).toContain("gpt-5.5"); + expect(seenArgs).toContain('service_tier="fast"'); }); it("codex does not leak lifecycle JSONL when no assistant text was produced", async () => {