diff --git a/codex-cli/src/utils/input-utils.ts b/codex-cli/src/utils/input-utils.ts index 80a238ba5..449f27cb4 100644 --- a/codex-cli/src/utils/input-utils.ts +++ b/codex-cli/src/utils/input-utils.ts @@ -2,6 +2,7 @@ import type { ResponseInputItem } from "openai/resources/responses/responses"; import { fileTypeFromBuffer } from "file-type"; import fs from "fs/promises"; +import path from "path"; export async function createInputItem( text: string, @@ -14,17 +15,24 @@ export async function createInputItem( }; for (const filePath of images) { - /* eslint-disable no-await-in-loop */ - const binary = await fs.readFile(filePath); - const kind = await fileTypeFromBuffer(binary); - /* eslint-enable no-await-in-loop */ - const encoded = binary.toString("base64"); - const mime = kind?.mime ?? "application/octet-stream"; - inputItem.content.push({ - type: "input_image", - detail: "auto", - image_url: `data:${mime};base64,${encoded}`, - }); + try { + /* eslint-disable no-await-in-loop */ + const binary = await fs.readFile(filePath); + const kind = await fileTypeFromBuffer(binary); + /* eslint-enable no-await-in-loop */ + const encoded = binary.toString("base64"); + const mime = kind?.mime ?? "application/octet-stream"; + inputItem.content.push({ + type: "input_image", + detail: "auto", + image_url: `data:${mime};base64,${encoded}`, + }); + } catch (err) { + inputItem.content.push({ + type: "input_text", + text: `[missing image: ${path.basename(filePath)}]`, + }); + } } return inputItem; diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts new file mode 100644 index 000000000..5290e5548 --- /dev/null +++ b/codex-cli/tests/input-utils.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, vi } from "vitest"; +import fs from "fs/promises"; +import { createInputItem } from "../src/utils/input-utils.js"; + +describe("createInputItem", () => { + it("returns only text when no images provided", async () => { + const result = await createInputItem("hello", []); + expect(result).toEqual({ + role: "user", + type: "message", + content: [{ type: "input_text", text: "hello" }], + }); + }); + + it("includes image content for existing file", async () => { + const fakeBuffer = Buffer.from("fake image content"); + const readSpy = vi.spyOn(fs, "readFile").mockResolvedValue(fakeBuffer as any); + const result = await createInputItem("hello", ["dummy-path"]); + const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString("base64")}`; + expect(result.role).toBe("user"); + expect(result.type).toBe("message"); + expect(result.content.length).toBe(2); + const [textItem, imageItem] = result.content; + expect(textItem).toEqual({ type: "input_text", text: "hello" }); + expect(imageItem).toEqual({ + type: "input_image", + detail: "auto", + image_url: expectedUrl, + }); + readSpy.mockRestore(); + }); + + it("falls back to missing image text for non-existent file", async () => { + const filePath = "tests/__fixtures__/does-not-exist.png"; + const result = await createInputItem("hello", [filePath]); + expect(result.content.length).toBe(2); + const fallbackItem = result.content[1]; + expect(fallbackItem).toEqual({ + type: "input_text", + text: "[missing image: does-not-exist.png]", + }); + }); +}); \ No newline at end of file