/** * @file AiToolConfig.test.tsx * @description The generic per-tool Settings tab: the tool's path, homepage * and an Update link to Settings → Agents; its session count, cost and * lanes; a default-model picker saved through PUT /api/ai-tools/:id; and its * catalog-declared config files, edited over the sha they were opened at with * the server's refusal shown. * @author Nguyễn Ngọc Trí Vĩ */ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { api, type AiTool } from "../../lib/api"; import { __resetAiTools } from "../../hooks/useAiTools"; import { AiToolConfig } from "../AiToolConfig"; vi.mock("../../lib/api", () => ({ api: { aiTools: { list: vi.fn(), update: vi.fn(), models: vi.fn(), usage: vi.fn(), config: vi.fn(), readConfig: vi.fn(), saveConfig: vi.fn(), iconUrl: (id: string) => `/api/ai-tools/${id}/icon`, }, lanes: { list: vi.fn() }, }, RUN_MODEL_CHOICES: [], })); vi.mock("../../components/settings/PricingSettings", () => ({ PricingSettings: ({ vendor }: { vendor: string }) =>
pricing:{vendor}
, })); const GEMINI: AiTool = { id: "gemini", label: "Gemini", command: "gemini", homepageUrl: "https://github.com/google-gemini/gemini-cli", installed: true, path: "/usr/bin/gemini", enabled: true, source: "builtin", launcher: null, prompt: { mode: "flag" }, resume: false, bypass: true, routerCapable: false, install: { kind: "npm", package: "@google/gemini-cli" }, update: { kind: "npm" }, modelFlag: true, defaultModel: null, modelVendor: "google", configFiles: 2, }; beforeEach(() => { __resetAiTools(); const view = { default: "claude", tools: [GEMINI] }; vi.mocked(api.aiTools.list) .mockReset() .mockResolvedValue(view as never); vi.mocked(api.aiTools.update) .mockReset() .mockResolvedValue(view as never); vi.mocked(api.aiTools.models) .mockReset() .mockResolvedValue({ flag: true, models: [{ id: "gemini-3.1-pro", label: "gemini-3.1-pro" }], defaultModel: null, }); vi.mocked(api.aiTools.usage).mockReset().mockResolvedValue({ sessions: 3, cost: 1.5 }); vi.mocked(api.aiTools.config) .mockReset() .mockResolvedValue({ files: [ { index: 0, path: "/h/.gemini/settings.json", format: "json", exists: true, size: 2 }, { index: 1, path: "/h/.gemini/GEMINI.md", format: "text", exists: false, size: null }, ], }); vi.mocked(api.aiTools.readConfig) .mockReset() .mockResolvedValue({ path: "/h/.gemini/settings.json", format: "json", text: "{}", sha: "s1" }); vi.mocked(api.aiTools.saveConfig).mockReset(); vi.mocked(api.lanes.list) .mockReset() .mockResolvedValue({ lanes: [ { id: 7, tool: "gemini", title: "web", cwd: "/w" }, { id: 8, tool: "claude", title: "other", cwd: "/o" }, ], } as never); }); const renderTab = () => render( ); describe("AiToolConfig", () => { it("shows the tool, its usage, its lanes and its vendor's pricing", async () => { renderTab(); expect(await screen.findByText("/usr/bin/gemini")).toBeInTheDocument(); expect(await screen.findByText(/3 sessions/)).toBeInTheDocument(); expect(screen.getByRole("link", { name: "web" })).toHaveAttribute("href", "/workspace?lane=7"); expect(screen.queryByRole("link", { name: "other" })).toBeNull(); expect(screen.getByRole("link", { name: /update/i })).toHaveAttribute( "href", "/settings/agents" ); expect(screen.getByText("pricing:google")).toBeInTheDocument(); }); it("saves the default model", async () => { renderTab(); fireEvent.click(await screen.findByText("Inherit from settings")); fireEvent.click(await screen.findByText("gemini-3.1-pro")); fireEvent.click(screen.getByRole("button", { name: /save default model/i })); await waitFor(() => expect(api.aiTools.update).toHaveBeenCalledWith("gemini", { defaultModel: "gemini-3.1-pro" }) ); }); it("edits a config file over the sha it opened, and shows a refusal", async () => { vi.mocked(api.aiTools.saveConfig).mockRejectedValueOnce(new Error("not valid JSON")); renderTab(); fireEvent.click(await screen.findByRole("button", { name: "/h/.gemini/settings.json" })); expect(screen.getByRole("button", { name: "/h/.gemini/GEMINI.md" })).toBeDisabled(); const box = await screen.findByRole("textbox", { name: "/h/.gemini/settings.json" }); fireEvent.change(box, { target: { value: "{nope" } }); fireEvent.click(screen.getByRole("button", { name: /^save$/i })); expect(await screen.findByText(/not valid JSON/)).toBeInTheDocument(); expect(api.aiTools.saveConfig).toHaveBeenCalledWith("gemini", 0, "{nope", "s1"); }); });