/** * @file PricingSettings.test.tsx * @description The shared Model Pricing table: each tool's tab shows its own * vendor's rows, **Show all models** lists the rest, a row added outside the * filter turns that switch on instead of vanishing, and Reset Defaults is * offered only where asked for (the Claude tab). * @author Nguyễn Ngọc Trí Vĩ */ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import i18n from "i18next"; import { PricingSettings } from "../PricingSettings"; import { api } from "../../../lib/api"; import type { ModelPricing } from "../../../lib/types"; vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { pricing: { list: vi.fn(), upsert: vi.fn(), delete: vi.fn() }, settings: { resetPricing: vi.fn() }, }, }; }); const rule = (model_pattern: string, display_name: string) => ({ model_pattern, display_name, input_per_mtok: 1, output_per_mtok: 2, cache_read_per_mtok: 0.1, cache_write_per_mtok: 0, cache_write_1h_per_mtok: 0, fast_input_per_mtok: 0, fast_output_per_mtok: 0, updated_at: "2026-09-29 10:00:00", }) as ModelPricing; const ROWS = [rule("claude-opus-5-5%", "Claude Opus 5.5"), rule("gpt-5.6-sol%", "GPT-5.6 Sol")]; beforeEach(() => { i18n.changeLanguage("en"); vi.mocked(api.pricing.list).mockReset().mockResolvedValue({ pricing: ROWS }); vi.mocked(api.pricing.upsert) .mockReset() .mockResolvedValue({} as never); }); describe("PricingSettings", () => { it("shows the vendor's rows, and the rest behind Show all models", async () => { const user = userEvent.setup(); render(); expect(await screen.findByText("gpt-5.6-sol%")).toBeInTheDocument(); expect(screen.queryByText("claude-opus-5-5%")).toBeNull(); expect(screen.queryByRole("button", { name: /Reset Defaults/i })).toBeNull(); await user.click(screen.getByRole("checkbox", { name: /Show all models \(1 more\)/ })); expect(screen.getByText("claude-opus-5-5%")).toBeInTheDocument(); }); it("marks a row the automatic sync keeps up to date", async () => { vi.mocked(api.pricing.list).mockResolvedValue({ pricing: [ { ...rule("gpt-6-sol%", "GPT-6 Sol"), source: "sync" }, { ...rule("gpt-5.6-sol%", "GPT-5.6 Sol"), source: "user" }, ], }); render(); await screen.findByText("gpt-6-sol%"); expect(screen.getAllByText("Auto")).toHaveLength(1); }); it("says where OpenAI prices come from when there are none", async () => { vi.mocked(api.pricing.list).mockResolvedValue({ pricing: [ROWS[0]!] }); render(); expect(await screen.findByText(/added automatically from LiteLLM/)).toBeInTheDocument(); }); it("turns Show all on when a row outside the filter is added", async () => { const user = userEvent.setup(); render(); await screen.findByText("gpt-5.6-sol%"); await user.click(screen.getByRole("button", { name: /Add Model/i })); const [pattern, name] = screen.getAllByRole("textbox"); await user.type(pattern!, "deepseek-v4%"); await user.type(name!, "DeepSeek V4"); vi.mocked(api.pricing.list).mockResolvedValue({ pricing: [...ROWS, rule("deepseek-v4%", "DeepSeek V4")], }); await user.click(screen.getByTitle(/save/i)); expect(await screen.findByText("deepseek-v4%")).toBeInTheDocument(); expect(screen.getByRole("checkbox", { name: /Show all models/ })).toBeChecked(); }); it("offers Reset Defaults on the Claude tab, confirmed by a second click", async () => { const user = userEvent.setup(); vi.mocked(api.settings.resetPricing).mockReset().mockResolvedValue({ ok: true, pricing: ROWS }); render(); expect(await screen.findByText("claude-opus-5-5%")).toBeInTheDocument(); expect(screen.queryByText("gpt-5.6-sol%")).toBeNull(); await user.click(screen.getByRole("button", { name: /Reset Defaults/i })); expect(api.settings.resetPricing).not.toHaveBeenCalled(); await user.click(screen.getByRole("button", { name: /confirm/i })); expect(api.settings.resetPricing).toHaveBeenCalledTimes(1); expect(api.settings.resetPricing).toHaveBeenCalledWith("claude"); }); });