diff --git a/ui/src/pages/Costs.test.tsx b/ui/src/pages/Costs.test.tsx index d7f21fd8da..ac21351ad3 100644 --- a/ui/src/pages/Costs.test.tsx +++ b/ui/src/pages/Costs.test.tsx @@ -241,7 +241,7 @@ describe("Shared Costs surfaces", () => { } finally { queryClient.clear(); vi.useRealTimers(); } }); - it.each(surfaces)("shows incomplete accounting and currency boundaries on the %s page", async (_name, props) => { + it.each(surfaces)("explains missing costs and pending runs neutrally on the %s page", async (_name, props) => { for (const mock of Object.values(costsApiMocks)) mock.mockResolvedValue([]); costsApiMocks.byUser.mockResolvedValue({ activeUserCount: 1, rows: [] }); costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 2, pendingRunCount: 1 }); @@ -254,9 +254,26 @@ describe("Shared Costs surfaces", () => { root.render(); }); await act(async () => { - await vi.waitFor(() => expect(container.textContent).toContain("Spend is incomplete: 2 usage events have no reliable price; 1 runs await accounting.")); + await vi.waitFor(() => expect(container.textContent).toContain("Costs are unavailable for 2 usage entries in this period. Totals include known costs only.")); }); + const notice = [...container.querySelectorAll('[role="status"]')].find(element => element.textContent?.includes("Costs are unavailable"))!; + expect(notice.classList.contains("text-muted-foreground")).toBe(true); + expect(notice.querySelector(".text-destructive")).toBeNull(); + expect(notice.classList.contains("text-destructive")).toBe(false); + expect(notice.textContent).toContain("1 run is awaiting cost data."); + expect(notice.previousElementSibling?.textContent).toContain("Inference spend"); expect(container.textContent).toContain("Finance headline totals are USD only"); + costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 10, pendingRunCount: 0 }); + await act(async () => { await queryClient.invalidateQueries(); }); + await vi.waitFor(() => expect(notice.textContent).toContain("Costs are unavailable for 10 usage entries")); + expect(notice.textContent).not.toContain("awaiting cost data"); + expect(notice.textContent).not.toContain("0 runs"); + costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: false, unpricedEventCount: 0, pendingRunCount: 1 }); + await act(async () => { await queryClient.invalidateQueries(); }); + await vi.waitFor(() => expect(notice.textContent).toBe("1 run is awaiting cost data.")); + costsApiMocks.summary.mockResolvedValue({ spendCents: 12.4, budgetCents: 0, pricingComplete: true, unpricedEventCount: 0, pendingRunCount: 0 }); + await act(async () => { await queryClient.invalidateQueries(); }); + await vi.waitFor(() => expect(container.contains(notice)).toBe(false)); }); it.each(surfaces)("labels each agent and expanded model independently on the %s page", async (_name, props) => { diff --git a/ui/src/pages/Costs.tsx b/ui/src/pages/Costs.tsx index 3f951d0d2d..56330c26f8 100644 --- a/ui/src/pages/Costs.tsx +++ b/ui/src/pages/Costs.tsx @@ -622,11 +622,6 @@ export function Costs({ ) : null} - {spendData?.summary.pricingComplete === false && ( -

- Spend is incomplete: {spendData.summary.unpricedEventCount} usage events have no reliable price; {spendData.summary.pendingRunCount} runs await accounting. Known spend is shown below. -

- )} {financeData?.summary.currencies?.some((row) => row.currency !== "USD") && (

Finance headline totals are USD only. Other currencies are listed separately; no exchange-rate conversion is applied. @@ -673,6 +668,20 @@ export function Costs({ icon={ArrowUpRight} /> + {spendData?.summary.pricingComplete === false && ( +

+ {spendData.summary.unpricedEventCount > 0 && ( +

+ Costs are unavailable for {spendData.summary.unpricedEventCount} usage {spendData.summary.unpricedEventCount === 1 ? "entry" : "entries"} in this period. Totals include known costs only. +

+ )} + {spendData.summary.pendingRunCount > 0 && ( +

+ {spendData.summary.pendingRunCount} {spendData.summary.pendingRunCount === 1 ? "run is" : "runs are"} awaiting cost data. +

+ )} +
+ )} ) : null}