import { describe, expect, it, vi, beforeEach } from "vitest"; import { screen, waitFor } from "@testing-library/react"; import { renderWithProviders } from "@/test/utils"; import userEvent from "@testing-library/user-event"; const listProviders = vi.fn(); const createProvider = vi.fn(); const applyProvider = vi.fn(); const restore = vi.fn(); const getStatus = vi.fn(); const fetchModels = vi.fn(); vi.mock("@/lib/api", () => ({ api: { listProviders: (target: string) => listProviders(target), createProvider: (input: unknown) => createProvider(input), updateProvider: vi.fn(), deleteProvider: vi.fn(), applyProvider: (id: string, target: string) => applyProvider(id, target), restore: (input: unknown) => restore(input), getStatus: () => getStatus(), getSettings: vi.fn().mockResolvedValue({ language: "zh-CN", port_mode: "auto", fixed_port: 0 }), updateSettings: vi.fn(), listLogs: vi.fn().mockResolvedValue({ items: [] }), listBackups: vi.fn().mockResolvedValue({ items: [] }), fetchModels: (input: unknown) => fetchModels(input), }, })); import Codex from "./Codex"; const provider = { id: "p_1", name: "DeepSeek", preset: "deepseek", base_url: "https://api.deepseek.com/v1", api_key: "sk-x", models: { default: "deepseek-chat", sonnet: "deepseek-chat", haiku: "deepseek-chat", opus: "deepseek-chat" }, enabled_for_claude: false, enabled_for_codex: false, target: "codex", created_at: "2026-10-18T00:00:00Z", }; beforeEach(() => { vi.clearAllMocks(); listProviders.mockResolvedValue({ items: [provider] }); getStatus.mockResolvedValue({ running: true, version: "0.1.0", port: 52917, uptime_sec: 95, active_claude_provider: null, active_codex_provider: null, stats: { requests_total: 0, requests_failed: 0, input_tokens: 0, output_tokens: 0 }, language: "zh-CN", port_mode: "auto", fixed_port: 0, }); createProvider.mockResolvedValue({ ...provider, id: "p_2" }); applyProvider.mockResolvedValue({ provider, injection: { model_provider: "crossrelay", table: { name: "crossrelay", wire_api: "responses", base_url: "http://127.0.0.1:52917/v1", experimental_bearer_token: "crl_x", requires_openai_auth: "false", }, }, }); restore.mockResolvedValue({ restored_from: "" }); fetchModels.mockResolvedValue({ items: [{ id: "deepseek-chat" }, { id: "deepseek-reasoner" }] }); }); describe("Codex page", () => { it("shows the injection status, config path and provider list", async () => { renderWithProviders(); expect(await screen.findByText("未注入")).toBeInTheDocument(); expect(screen.getByText(/config\.toml/)).toBeInTheDocument(); expect(screen.getByText("DeepSeek")).toBeInTheDocument(); }); it("requests only Codex providers", async () => { renderWithProviders(); await screen.findByText("DeepSeek"); expect(listProviders).toHaveBeenCalledWith("codex"); }); it("creates a provider scoped to Codex", async () => { const user = userEvent.setup(); renderWithProviders(); await user.click(await screen.findByRole("button", { name: /新增供应商|Add provider/ })); await user.type(screen.getByLabelText(/名称|Name/), "Kimi"); await user.type(screen.getByLabelText(/Base URL/), "https://api.moonshot.cn/v1"); await user.type(screen.getByLabelText(/API Key/), "sk-kimi"); await user.click(screen.getByRole("button", { name: /保存|Save/ })); await waitFor(() => expect(createProvider).toHaveBeenCalledTimes(1)); expect(createProvider.mock.calls[0][0]).toMatchObject({ name: "Kimi", base_url: "https://api.moonshot.cn/v1", target: "codex", }); }); it("offers model fetching instead of Claude role fields", async () => { const user = userEvent.setup(); renderWithProviders(); await user.click(await screen.findByRole("button", { name: /新增供应商|Add provider/ })); expect(screen.queryByLabelText(/Sonnet 模型|Sonnet model/)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/Haiku 模型|Haiku model/)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/Opus 模型|Opus model/)).not.toBeInTheDocument(); await user.type(screen.getByLabelText(/Base URL/), "https://api.deepseek.com/v1"); await user.type(screen.getByLabelText(/API Key/), "sk-x"); await user.click(screen.getByRole("button", { name: /获取模型列表|Fetch models/ })); await waitFor(() => expect(fetchModels).toHaveBeenCalledWith({ base_url: "https://api.deepseek.com/v1", api_key: "sk-x", }), ); expect(await screen.findByText(/已获取 2 个模型|Fetched 2 models/)).toBeInTheDocument(); }); it("activates a provider for Codex and previews the injected keys", async () => { const user = userEvent.setup(); renderWithProviders(); await user.click(await screen.findByRole("button", { name: /激活并写入|Activate & apply/ })); await waitFor(() => expect(applyProvider).toHaveBeenCalledWith("p_1", "codex")); expect(await screen.findByText(/experimental_bearer_token/)).toBeInTheDocument(); expect(screen.getByText(/wire_api/)).toBeInTheDocument(); }); it("restores the official config", async () => { const user = userEvent.setup(); renderWithProviders(); await user.click(await screen.findByRole("button", { name: /还原官方配置|Restore official config/ })); await waitFor(() => expect(restore).toHaveBeenCalledWith({ target: "codex" })); }); it("renders protocol options and submits selected protocol for Codex", async () => { const user = userEvent.setup(); renderWithProviders(); await user.click(await screen.findByRole("button", { name: /新增供应商|Add provider/ })); const select = screen.getByLabelText(/协议选择|Protocol/); expect(select).toBeInTheDocument(); expect(screen.getByRole("option", { name: /OpenAI Responses API/ })).toBeInTheDocument(); expect(screen.getByRole("option", { name: /OpenAI Chat Completions/ })).toBeInTheDocument(); expect(screen.getByRole("option", { name: /Anthropic Messages/ })).toBeInTheDocument(); await user.selectOptions(select, "openai_responses"); await user.type(screen.getByLabelText(/名称|Name/), "OpenAI-Direct"); await user.type(screen.getByLabelText(/Base URL/), "https://api.openai.com/v1"); await user.type(screen.getByLabelText(/API Key/), "sk-oa"); await user.click(screen.getByRole("button", { name: /保存|Save/ })); await waitFor(() => expect(createProvider).toHaveBeenCalledTimes(1)); const input = createProvider.mock.calls[0][0]; expect(input).toMatchObject({ name: "OpenAI-Direct", protocol: "openai_responses", target: "codex", }); }); });