import { describe, it, expect, vi, beforeEach } from "vitest"; import { CodexProvider } from "@/providers/codex-provider.js"; import { CodexCliDetector } from "@/providers/codex-cli-detector.js"; import { codexConfigManager } from "@/providers/codex-config-manager.js"; import * as subprocessManager from "@/lib/subprocess-manager.js"; import { collectAsyncGenerator } from "../../utils/helpers.js"; vi.mock("@/providers/codex-cli-detector.js"); vi.mock("@/providers/codex-config-manager.js"); vi.mock("@/lib/subprocess-manager.js"); describe("codex-provider.ts", () => { let provider: CodexProvider; beforeEach(() => { vi.clearAllMocks(); provider = new CodexProvider(); delete process.env.OPENAI_API_KEY; delete process.env.CODEX_CLI_PATH; }); describe("getName", () => { it("should return 'codex' as provider name", () => { expect(provider.getName()).toBe("codex"); }); }); describe("executeQuery", () => { it("should use default 'codex' when CLI not detected", async () => { // When CLI is not detected, findCodexPath returns "codex" as default vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: false, }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Hello", cwd: "/test", }); const results = await collectAsyncGenerator(generator); // Should succeed with default "codex" path const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; expect(call.command).toBe("codex"); }); it("should error when not authenticated", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: false, method: "none", hasAuthFile: false, hasEnvKey: false, }); const generator = provider.executeQuery({ prompt: "Hello", cwd: "/test", }); const results = await collectAsyncGenerator(generator); expect(results).toHaveLength(1); expect(results[0]).toMatchObject({ type: "error", error: expect.stringContaining("not authenticated"), }); }); it("should execute query with CLI auth", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); const mockEvents = [ { type: "thread.started" }, { type: "item.completed", item: { type: "message", content: "Response" } }, { type: "thread.completed" }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Hello", cwd: "/test", }); const results = await collectAsyncGenerator(generator); expect(results).toHaveLength(3); // message + thread completion + final success expect(results[0].type).toBe("assistant"); expect(results[1]).toMatchObject({ type: "result", subtype: "success" }); expect(results[2]).toMatchObject({ type: "result", subtype: "success" }); }); it("should execute query with API key", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: false, method: "none", hasAuthFile: false, hasEnvKey: false, }); process.env.OPENAI_API_KEY = "test-api-key"; const mockEvents = [{ type: "thread.completed" }]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test", }); const results = await collectAsyncGenerator(generator); expect(results).toHaveLength(2); // thread completion + final success expect(subprocessManager.spawnJSONLProcess).toHaveBeenCalledWith( expect.objectContaining({ env: expect.objectContaining({ OPENAI_API_KEY: "test-api-key", }), }) ); }); it("should spawn subprocess with correct arguments", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Test prompt", model: "gpt-5.2", cwd: "/test/dir", }); await collectAsyncGenerator(generator); expect(subprocessManager.spawnJSONLProcess).toHaveBeenCalledWith({ command: "/usr/bin/codex", args: ["exec", "--model", "gpt-5.2", "--json", "--full-auto", "Test prompt"], cwd: "/test/dir", env: {}, abortController: undefined, timeout: 30000, }); }); it("should prepend system prompt", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "User request", systemPrompt: "You are helpful", cwd: "/test", }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; const combinedPrompt = call.args[call.args.length - 1]; expect(combinedPrompt).toContain("You are helpful"); expect(combinedPrompt).toContain("---"); expect(combinedPrompt).toContain("User request"); }); it("should handle conversation history", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const conversationHistory = [ { role: "user" as const, content: "Previous message" }, { role: "assistant" as const, content: "Previous response" }, ]; const generator = provider.executeQuery({ prompt: "Current message", conversationHistory, cwd: "/test", }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; const combinedPrompt = call.args[call.args.length - 1]; expect(combinedPrompt).toContain("Previous message"); expect(combinedPrompt).toContain("Previous response"); expect(combinedPrompt).toContain("Current request:"); expect(combinedPrompt).toContain("Current message"); }); it("should extract text from array prompt (ignore images)", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const arrayPrompt = [ { type: "text", text: "Text part 1" }, { type: "image", source: { type: "base64", data: "..." } }, { type: "text", text: "Text part 2" }, ]; const generator = provider.executeQuery({ prompt: arrayPrompt as any, cwd: "/test", }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; const combinedPrompt = call.args[call.args.length - 1]; expect(combinedPrompt).toContain("Text part 1"); expect(combinedPrompt).toContain("Text part 2"); expect(combinedPrompt).not.toContain("image"); }); it("should configure MCP server if provided", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); vi.mocked(codexConfigManager.configureMcpServer).mockResolvedValue( "/path/config.toml" ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test", mcpServers: { "automaker-tools": { command: "node", args: ["server.js"], }, }, }); await collectAsyncGenerator(generator); // Note: getMcpServerPath currently returns null, so configureMcpServer won't be called // This test verifies the code path exists even if not fully implemented expect(true).toBe(true); }); it("should handle subprocess errors", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { throw new Error("Process failed"); })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test", }); const results = await collectAsyncGenerator(generator); expect(results).toHaveLength(1); expect(results[0]).toMatchObject({ type: "error", error: "Process failed", }); }); it("should use default model gpt-5.2", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test", }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; expect(call.args).toContain("gpt-5.2"); }); }); describe("event conversion", () => { beforeEach(() => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); }); it("should convert reasoning item to thinking message", async () => { const mockEvents = [ { type: "item.completed", item: { type: "reasoning", text: "Let me think about this...", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0]).toMatchObject({ type: "assistant", message: { role: "assistant", content: [ { type: "thinking", thinking: "Let me think about this...", }, ], }, }); }); it("should convert agent_message to text message", async () => { const mockEvents = [ { type: "item.completed", item: { type: "agent_message", content: "Here is the response", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0]).toMatchObject({ type: "assistant", message: { role: "assistant", content: [ { type: "text", text: "Here is the response", }, ], }, }); }); it("should convert message type to text message", async () => { const mockEvents = [ { type: "item.completed", item: { type: "message", text: "Message text", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0].type).toBe("assistant"); expect(results[0].message?.content[0]).toMatchObject({ type: "text", text: "Message text", }); }); it("should convert command_execution to formatted text", async () => { const mockEvents = [ { type: "item.completed", item: { type: "command_execution", command: "ls -la", aggregated_output: "file1.txt\nfile2.txt", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); const content = results[0].message?.content[0] as any; expect(content.type).toBe("text"); expect(content.text).toContain("```bash"); expect(content.text).toContain("ls -la"); expect(content.text).toContain("file1.txt"); }); it("should convert tool_use item", async () => { const mockEvents = [ { type: "item.completed", item: { type: "tool_use", tool: "read_file", input: { path: "/test.txt" }, }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0].message?.content[0]).toMatchObject({ type: "tool_use", name: "read_file", input: { path: "/test.txt" }, }); }); it("should convert tool_result item", async () => { const mockEvents = [ { type: "item.completed", item: { type: "tool_result", tool_use_id: "123", output: "File contents", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0].message?.content[0]).toMatchObject({ type: "tool_result", tool_use_id: "123", content: "File contents", }); }); it("should convert todo_list to formatted text", async () => { const mockEvents = [ { type: "item.completed", item: { type: "todo_list", items: [{ text: "Task 1" }, { text: "Task 2" }], }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); const content = results[0].message?.content[0] as any; expect(content.type).toBe("text"); expect(content.text).toContain("**Todo List:**"); expect(content.text).toContain("1. Task 1"); expect(content.text).toContain("2. Task 2"); }); it("should convert file_change to formatted text", async () => { const mockEvents = [ { type: "item.completed", item: { type: "file_change", changes: [{ path: "/file1.txt" }, { path: "/file2.txt" }], }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); const content = results[0].message?.content[0] as any; expect(content.type).toBe("text"); expect(content.text).toContain("**File Changes:**"); expect(content.text).toContain("Modified: /file1.txt"); expect(content.text).toContain("Modified: /file2.txt"); }); it("should handle item.started with command_execution", async () => { const mockEvents = [ { type: "item.started", item: { type: "command_execution", command: "npm install", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0].message?.content[0]).toMatchObject({ type: "tool_use", name: "bash", input: { command: "npm install" }, }); }); it("should handle item.started with todo_list", async () => { const mockEvents = [ { type: "item.started", item: { type: "todo_list", items: ["Task 1", "Task 2"], }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); const content = results[0].message?.content[0] as any; expect(content.text).toContain("**Todo List:**"); expect(content.text).toContain("1. Task 1"); expect(content.text).toContain("2. Task 2"); }); it("should handle item.updated with todo_list", async () => { const mockEvents = [ { type: "item.updated", item: { type: "todo_list", items: [ { text: "Task 1", status: "completed" }, { text: "Task 2", status: "pending" }, ], }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); const content = results[0].message?.content[0] as any; expect(content.text).toContain("**Updated Todo List:**"); expect(content.text).toContain("1. [✓] Task 1"); expect(content.text).toContain("2. [ ] Task 2"); }); it("should convert error events", async () => { const mockEvents = [ { type: "error", data: { message: "Something went wrong" }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0]).toMatchObject({ type: "error", error: "Something went wrong", }); }); it("should convert thread.completed to result", async () => { const mockEvents = [{ type: "thread.completed" }]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0]).toMatchObject({ type: "result", subtype: "success", }); }); it("should skip thread.started events", async () => { const mockEvents = [ { type: "thread.started" }, { type: "item.completed", item: { type: "message", content: "Response" }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); // Should only have the message and final success expect(results).toHaveLength(2); expect(results[0].type).toBe("assistant"); }); it("should skip turn events", async () => { const mockEvents = [ { type: "turn.started" }, { type: "item.completed", item: { type: "message", content: "Response" }, }, { type: "turn.completed" }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); // Should only have the message and final success expect(results).toHaveLength(2); expect(results[0].type).toBe("assistant"); }); it("should handle generic item with text fallback", async () => { const mockEvents = [ { type: "item.completed", item: { type: "unknown_type", text: "Generic output", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0].message?.content[0]).toMatchObject({ type: "text", text: "Generic output", }); }); it("should handle items with item_type field", async () => { const mockEvents = [ { type: "item.completed", item: { item_type: "message", content: "Using item_type field", }, }, ]; vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { for (const event of mockEvents) { yield event; } })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); const results = await collectAsyncGenerator(generator); expect(results[0].message?.content[0]).toMatchObject({ type: "text", text: "Using item_type field", }); }); }); describe("findCodexPath", () => { it("should use config.cliPath if set", async () => { provider.setConfig({ cliPath: "/custom/path/to/codex" }); vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; expect(call.command).toBe("/custom/path/to/codex"); }); it("should use CODEX_CLI_PATH env var if set", async () => { process.env.CODEX_CLI_PATH = "/env/path/to/codex"; vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; expect(call.command).toBe("/env/path/to/codex"); }); it("should auto-detect CLI path", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; expect(call.command).toBe("/usr/bin/codex"); }); it("should default to 'codex' if not detected", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: false, }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); vi.spyOn(subprocessManager, "spawnJSONLProcess").mockReturnValue( (async function* () { yield { type: "thread.completed" }; })() ); const generator = provider.executeQuery({ prompt: "Test", cwd: "/test" }); await collectAsyncGenerator(generator); const call = vi.mocked(subprocessManager.spawnJSONLProcess).mock.calls[0][0]; expect(call.command).toBe("codex"); }); }); describe("detectInstallation", () => { it("should combine detection and auth results", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: true, path: "/usr/bin/codex", version: "0.5.0", method: "cli", }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: true, method: "cli_verified", hasAuthFile: true, hasEnvKey: false, }); const result = await provider.detectInstallation(); expect(result).toMatchObject({ installed: true, path: "/usr/bin/codex", version: "0.5.0", method: "cli", hasApiKey: true, authenticated: true, }); }); it("should detect API key from env", async () => { vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({ installed: false, }); vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({ authenticated: false, method: "none", hasAuthFile: false, hasEnvKey: true, }); const result = await provider.detectInstallation(); expect(result.hasApiKey).toBe(true); expect(result.authenticated).toBe(false); }); }); describe("getAvailableModels", () => { it("should return 5 Codex models", () => { const models = provider.getAvailableModels(); expect(models).toHaveLength(5); }); it("should include gpt-5.2 as default", () => { const models = provider.getAvailableModels(); const gpt52 = models.find((m) => m.id === "gpt-5.2"); expect(gpt52).toBeDefined(); expect(gpt52?.name).toBe("GPT-5.2 (Codex)"); expect(gpt52?.default).toBe(true); expect(gpt52?.provider).toBe("openai-codex"); }); it("should include all expected models", () => { const models = provider.getAvailableModels(); const modelIds = models.map((m) => m.id); expect(modelIds).toContain("gpt-5.2"); expect(modelIds).toContain("gpt-5.1-codex-max"); expect(modelIds).toContain("gpt-5.1-codex"); expect(modelIds).toContain("gpt-5.1-codex-mini"); expect(modelIds).toContain("gpt-5.1"); }); it("should have correct capabilities", () => { const models = provider.getAvailableModels(); models.forEach((model) => { expect(model.supportsTools).toBe(true); expect(model.contextWindow).toBe(256000); expect(model.modelString).toBe(model.id); }); }); it("should have vision support except for mini", () => { const models = provider.getAvailableModels(); const mini = models.find((m) => m.id === "gpt-5.1-codex-mini"); const others = models.filter((m) => m.id !== "gpt-5.1-codex-mini"); expect(mini?.supportsVision).toBe(false); others.forEach((model) => { expect(model.supportsVision).toBe(true); }); }); }); describe("supportsFeature", () => { it("should support tools feature", () => { expect(provider.supportsFeature("tools")).toBe(true); }); it("should support text feature", () => { expect(provider.supportsFeature("text")).toBe(true); }); it("should support vision feature", () => { expect(provider.supportsFeature("vision")).toBe(true); }); it("should support mcp feature", () => { expect(provider.supportsFeature("mcp")).toBe(true); }); it("should support cli feature", () => { expect(provider.supportsFeature("cli")).toBe(true); }); it("should not support unknown features", () => { expect(provider.supportsFeature("unknown")).toBe(false); }); it("should not support thinking feature", () => { expect(provider.supportsFeature("thinking")).toBe(false); }); }); });