/** * @file ClaudeAgentHubSettings.test.tsx * @description Settings → Claude Code → AgentHub: the pricing table, the hook * status with Reinstall, and CLAUDE_HOME — the three sections moved out of * Settings → Dashboard (#40 part B) — still load and act as they did there. * @author Nguyễn Ngọc Trí Vĩ */ import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import i18n from "i18next"; import { ClaudeAgentHubSettings } from "../ClaudeAgentHubSettings"; import { api } from "../../../lib/api"; import type { ModelPricing } from "../../../lib/types"; import { settleConfirm, subscribeConfirm } from "../../../lib/confirm"; /** Say yes to every confirm (a CLAUDE_HOME change now asks first). */ function confirmYes() { return subscribeConfirm((p) => p && queueMicrotask(() => settleConfirm(true))); } vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { pricing: { list: vi.fn(), upsert: vi.fn(), delete: vi.fn() }, settings: { info: vi.fn(), reinstallHooks: vi.fn(), resetPricing: vi.fn(), claudeHome: { get: vi.fn(), set: vi.fn() }, }, }, }; }); const RULE = { model_pattern: "claude-opus-5-5%", display_name: "Claude Opus 5.5", input_per_mtok: 4, output_per_mtok: 20, cache_read_per_mtok: 0.2, cache_write_per_mtok: 5, cache_write_1h_per_mtok: 8, fast_input_per_mtok: 8, fast_output_per_mtok: 40, updated_at: "2026-09-28 10:00:00", } as ModelPricing; beforeEach(() => { i18n.changeLanguage("en"); vi.mocked(api.pricing.list) .mockReset() .mockResolvedValue({ pricing: [RULE] }); vi.mocked(api.settings.info) .mockReset() .mockResolvedValue({ hooks: { installed: true, path: "/home/u/.claude/settings.json", hooks: { SessionStart: true, Stop: true }, }, } as unknown as Awaited>); vi.mocked(api.settings.claudeHome.get) .mockReset() .mockResolvedValue({ claude_home: "/home/u/.claude" }); vi.mocked(api.settings.claudeHome.set) .mockReset() .mockResolvedValue({ ok: true, claude_home: "/srv/claude" }); vi.mocked(api.settings.resetPricing).mockReset(); vi.mocked(api.settings.reinstallHooks) .mockReset() .mockResolvedValue({ ok: true, hooks: { installed: true, hooks: {} } }); }); describe("ClaudeAgentHubSettings", () => { it("renders pricing, hook status and CLAUDE_HOME in their own sections", async () => { const { container } = render(); expect(await screen.findByText("claude-opus-5-5")).toBeInTheDocument(); for (const id of ["pricing", "hooks", "claude-home"]) { expect(container.querySelector(`section#${id}`)).not.toBeNull(); } const hooks = container.querySelector("section#hooks") as HTMLElement; expect(within(hooks).getByText("SessionStart")).toBeInTheDocument(); expect(within(hooks).getByText("/home/u/.claude/settings.json")).toBeInTheDocument(); expect(await screen.findByDisplayValue("/home/u/.claude")).toBeInTheDocument(); }); it("saves a new CLAUDE_HOME and reinstalls hooks", async () => { const user = userEvent.setup(); const { container } = render(); const input = await screen.findByDisplayValue("/home/u/.claude"); await user.clear(input); await user.type(input, "/srv/claude"); const home = container.querySelector("section#claude-home") as HTMLElement; const off = confirmYes(); await user.click(within(home).getByRole("button", { name: "Save" })); await waitFor(() => expect(api.settings.claudeHome.set).toHaveBeenCalledWith("/srv/claude")); off(); const hooks = container.querySelector("section#hooks") as HTMLElement; await user.click(within(hooks).getByRole("button")); expect(api.settings.reinstallHooks).toHaveBeenCalledTimes(1); }); it("re-reads hook status and tells the page after a CLAUDE_HOME save", async () => { const onHomeChanged = vi.fn(); const user = userEvent.setup(); const { container } = render(); const input = await screen.findByDisplayValue("/home/u/.claude"); await user.clear(input); await user.type(input, "/srv/claude"); const home = container.querySelector("section#claude-home") as HTMLElement; const reads = vi.mocked(api.settings.info).mock.calls.length; const off = confirmYes(); await user.click(within(home).getByRole("button", { name: "Save" })); await waitFor(() => expect(onHomeChanged).toHaveBeenCalledTimes(1)); off(); expect(vi.mocked(api.settings.info).mock.calls.length).toBeGreaterThan(reads); }); it("resets pricing only on the second click", async () => { const user = userEvent.setup(); vi.mocked(api.settings.resetPricing).mockResolvedValue({ ok: true, pricing: [RULE] }); render(); await user.click(await screen.findByRole("button", { name: "Reset Defaults" })); expect(api.settings.resetPricing).not.toHaveBeenCalled(); await user.click(screen.getByRole("button", { name: "Confirm Reset" })); expect(api.settings.resetPricing).toHaveBeenCalledTimes(1); expect(await screen.findByText("Reset to 1 default rules")).toBeInTheDocument(); }); });