// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { appearanceForPalette, agentAvatarUrl, type CostDataQualityItem, type CostDataQualityReason, type CostDataQualityReport } from "@paperclipai/shared"; import { ToastProvider, useToastState } from "../context/ToastContext"; import { CostDataQualityNotice } from "./CostDataQualityNotice"; const mocks = vi.hoisted(() => ({ dataQuality: vi.fn(), retryAccounting: vi.fn() })); vi.mock("../api/costs", () => ({ costsApi: mocks })); vi.mock("../context/CompanyContext", () => ({ useCompany: () => ({ selectedCompany: { issuePrefix: "PAP" } }) })); (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const key = ["costs", "company", "2026-10-01", "2026-10-09"]; function item(reason: CostDataQualityReason): CostDataQualityItem { return { id: reason, runId: `${reason}-run`, agentId: "codie", agentName: "Codie", occurredAt: "2026-10-09T12:00:00Z", provider: "openai", model: "gpt-6-astra", reason }; } function report(canRetry = true): CostDataQualityReport { return { canRetry, events: [item("unknown_billing"), item("missing_model"), item("missing_price")], runs: [item("waiting_for_receipt"), item("receipt_unavailable"), item("retryable")], hasMoreEvents: false, hasMoreRuns: false }; } function ToastStatus() { const toasts = useToastState(); return
{toasts.map(toast =>

{toast.title} {toast.body}

)}
; } describe("actionable cost notices", () => { let container: HTMLDivElement, root: ReturnType, client: QueryClient; beforeEach(() => { vi.resetAllMocks(); mocks.dataQuality.mockResolvedValue(report()); mocks.retryAccounting.mockResolvedValue({ accounted: true }); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); }); afterEach(() => { act(() => root.unmount()); client.clear(); container.remove(); }); async function render(counts: { unpricedCount?: number; pendingCount?: number; unknownBillingCount?: number } = {}) { await act(async () => root.render( )); } function button(text: string) { return [...container.querySelectorAll("button")].find(button => button.textContent === text)!; } async function click(text: string) { await act(async () => button(text).click()); } async function loaded() { await act(async () => vi.waitFor(() => expect(container.textContent).toContain("Ready to record"))); } it("loads bounded details only on demand using the displayed period", async () => { await render(); expect(mocks.dataQuality).not.toHaveBeenCalled(); expect(button("16 run costs unavailable").getAttribute("aria-expanded")).toBe("false"); expect(container.textContent).toBe("16 run costs unavailable"); expect(container.querySelector('svg[aria-hidden="true"]')).not.toBeNull(); expect(container.querySelector('[role="alert"]')).toBeNull(); await click("16 run costs unavailable"); await loaded(); expect(mocks.dataQuality).toHaveBeenCalledExactlyOnceWith("company", "2026-10-01", "2026-10-09"); expect(button("16 run costs unavailable").getAttribute("aria-expanded")).toBe("true"); const details = document.getElementById(button("16 run costs unavailable").getAttribute("aria-controls")!)!; expect(details.textContent).not.toContain("Totals include known charges only."); expect(details.textContent).not.toContain("12 entries without prices · 4 runs without final cost data · 9 entries with unknown billing"); expect(details.textContent).not.toContain("Includes 12 estimated charges."); await click("16 run costs unavailable"); expect(container.querySelector("ul")).toBeNull(); expect(container.textContent).toBe("16 run costs unavailable"); }); it("explains future configuration fixes and links to the affected runs in the active company", async () => { await render(); await click("16 run costs unavailable"); await loaded(); const row = (label: string) => [...container.querySelectorAll('[data-slot="badge"]')].find(badge => badge.textContent === label)!.closest("li")!; const billing = row("Billing category unknown"); expect(billing.textContent).toContain("Any recorded charges are included in the inference ledger."); const configuration = container.querySelector('a[href="/PAP/agents/codie/configuration"]')!; expect(configuration.textContent).toBe("Codie"); expect(configuration.closest("h3")!.textContent).toBe("Codie"); expect(container.querySelectorAll('a[href="/PAP/agents/codie/configuration"]')).toHaveLength(1); expect(billing.querySelector('a[href="/PAP/agents/codie/configuration"]')).toBeNull(); expect(billing.querySelector('a[href="/PAP/agents/codie/runs/unknown_billing-run"]')).not.toBeNull(); for (const link of container.querySelectorAll("a")) expect(link.hasAttribute("data-slot")).toBe(false); expect(container.querySelectorAll("h3")).toHaveLength(1); expect(container.querySelectorAll('[data-slot="agent-avatar"]')).toHaveLength(1); expect(container.textContent).toContain("Changing settings will not reprice this past run."); const runs = [row("Receiving usage"), row("Final usage unavailable")]; expect(runs[0].textContent).toContain("You do not need to rerun the agent."); expect(runs[1].textContent).toContain("Its cost remains unresolved for budget checks."); expect(runs[1].textContent).toContain("rerunning the agent will not recover this usage."); expect(runs[0].querySelector("button")).toBeNull(); expect(runs[1].querySelector("button")).toBeNull(); expect([...container.querySelectorAll("button")].filter(row => row.textContent === "Retry cost recording")).toHaveLength(1); const actionLine = button("Retry cost recording").parentElement!; expect(actionLine.children[0].tagName).toBe("TIME"); expect(actionLine.children[1].textContent).toBe("View run"); expect(actionLine.children[2].textContent).toBe("Retry cost recording"); expect(button("Retry cost recording").getAttribute("data-variant")).toBe("link"); }); it("groups all kinds of cost gaps by agent identity, alphabetizes names, and preserves appearances and newest-first entries", async () => { const appearance = appearanceForPalette("deep-tide"); mocks.dataQuality.mockResolvedValue({ canRetry: true, hasMoreEvents: false, hasMoreRuns: false, events: [ { ...item("missing_model"), agentId: "fry", agentName: "fry" }, { ...item("missing_price"), id: "shared-id", agentAppearance: appearance, occurredAt: "2026-10-08T12:00:00Z" }, { ...item("unknown_billing"), agentId: null, agentName: null, runId: null }, { ...item("missing_price"), id: "other-codie", agentId: "codie-2" }, ], runs: [ { ...item("receipt_unavailable"), agentId: "leela", agentName: "Leela" }, { ...item("retryable"), id: "shared-id", agentAppearance: appearance }, ], }); await render(); await click("16 run costs unavailable"); await loaded(); const groups = [...container.querySelectorAll('[aria-label="Cost details by agent"] > li')]; expect(groups.map(group => group.querySelector("h3")!.textContent)).toEqual(["Codie", "Codie", "fry", "Leela", "Paperclip services"]); const codieItems = groups[0].querySelectorAll("li"); expect(codieItems).toHaveLength(2); expect(codieItems[0].textContent).toContain("Ready to record"); expect(codieItems[1].textContent).toContain("Cost unavailable"); expect(groups[1].querySelectorAll("li")).toHaveLength(1); expect(groups[0].querySelector("img")?.getAttribute("src")).toBe(agentAvatarUrl(appearance, 20)); expect(groups[4].querySelector("a")).toBeNull(); }); it("does not label priced unknown billing or estimates as unavailable costs", async () => { await render({ unpricedCount: 0, pendingCount: 0, unknownBillingCount: 1 }); expect(container.textContent).toBe("1 billing type unknown"); await click("1 billing type unknown"); await loaded(); expect(container.textContent).toContain("Billing category unknown"); expect(container.textContent).not.toContain("Totals include known charges only."); expect(container.textContent).not.toContain("Includes 12 estimated charges."); }); it("records saved costs without rerunning work and refreshes every period for this company", async () => { let finish!: (result: { accounted: boolean }) => void; mocks.retryAccounting.mockImplementation(() => new Promise(resolve => { finish = resolve; })); await render(); await click("16 run costs unavailable"); await loaded(); const invalidate = vi.spyOn(client, "invalidateQueries"); await click("Retry cost recording"); await act(async () => vi.waitFor(() => expect(button("Retry cost recording").disabled).toBe(true))); await click("Retry cost recording"); expect(mocks.retryAccounting).toHaveBeenCalledTimes(1); await act(async () => finish({ accounted: true })); await act(async () => vi.waitFor(() => expect(container.textContent).toContain("Costs recorded."))); expect(mocks.retryAccounting).toHaveBeenCalledExactlyOnceWith("company", "retryable-run"); expect(invalidate).toHaveBeenCalledWith({ queryKey: ["costs", "company"] }); expect(invalidate).toHaveBeenCalledWith({ queryKey: ["cost-subscriptions", "company"] }); }); it("keeps retry confirmation after the refreshed report removes the run and the notice unmounts", async () => { mocks.dataQuality.mockResolvedValueOnce({ ...report(), events: [], runs: [item("retryable")] }) .mockResolvedValue({ ...report(), events: [], runs: [] }); await render({ unpricedCount: 0, pendingCount: 1, unknownBillingCount: 0 }); await click("1 run cost unavailable"); await loaded(); await click("Retry cost recording"); await act(async () => vi.waitFor(() => expect(container.textContent).toContain("No cost gaps remain"))); expect(container.textContent).not.toContain("Retry cost recording"); await act(async () => root.render( )); expect(container.textContent).toContain("Costs recorded. The agent was not rerun."); }); it("makes stale retry results understandable and never renders raw provider errors", async () => { mocks.retryAccounting.mockResolvedValueOnce({ accounted: false }).mockRejectedValueOnce(new Error("SECRET sh -c provider --token credential")); await render(); await click("16 run costs unavailable"); await loaded(); await click("Retry cost recording"); await act(async () => vi.waitFor(() => expect(container.textContent).toContain("No new cost data is available. The agent was not rerun."))); await click("Retry cost recording"); await act(async () => vi.waitFor(() => expect(container.textContent).toContain("Cost recording failed. Try again shortly."))); expect(container.textContent).not.toContain("SECRET"); expect(container.textContent).not.toContain("credential"); }); it("keeps viewer details readable without exposing the retry action", async () => { mocks.dataQuality.mockResolvedValue(report(false)); await render(); await click("16 run costs unavailable"); await loaded(); expect(container.textContent).not.toContain("Retry cost recording"); expect(mocks.retryAccounting).not.toHaveBeenCalled(); }); it("retains the last successful details through a failed background refresh", async () => { await render(); await click("16 run costs unavailable"); await loaded(); mocks.dataQuality.mockRejectedValue(new Error("SECRET backend exception")); await act(async () => { await client.invalidateQueries({ queryKey: [...key, "data-quality"] }); }); await act(async () => vi.waitFor(() => expect(container.textContent).toContain("Showing the last loaded cost details."))); expect(container.textContent).toContain("Ready to record"); expect(container.textContent).not.toContain("SECRET"); }); it("offers a safe first-load retry and explains truncated results", async () => { mocks.dataQuality.mockRejectedValueOnce(new Error("SECRET command")); await render(); await click("16 run costs unavailable"); await act(async () => vi.waitFor(() => expect(container.textContent).toContain("Cost details could not be loaded."))); expect(container.textContent).not.toContain("SECRET"); mocks.dataQuality.mockResolvedValue({ ...report(), hasMoreRuns: true }); await click("Retry loading"); await loaded(); expect(container.textContent).toContain("Select a shorter date range to inspect more."); }); });