mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-03 09:37:19 +01:00
fix: show initial budget load failures across cost views
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e1db48f754
commit
e26b758564
3 files changed
+42
-7
No files matched your search
@@ -160,7 +160,7 @@ Company quota requests resolve the caller's accessible subscription AI connectio
|
||||
|
||||
Responses carry an opaque account identity and successful capture timestamp. The 30-second cache is scoped to company, user, connection, grant and credential revision; authorization and revision metadata are checked before reading cached results. Transient failures retain the last successful UI observation with its original timestamp. Rotation, revocation and explicit authentication rejection clear obsolete windows. Public messages never contain provider subprocess commands or raw diagnostics.
|
||||
|
||||
Both Costs page variants retain loaded reports and expanded rows during background refresh failures, with a small stale-data notice. Initial failures remain visible. Company/date changes isolate cached reports. The run-count label is `runs: 0 api · 11 sub`: distinct recorded runs, not tasks or individual model requests. Input totals include cache reads, with the cached portion shown explicitly.
|
||||
Both Costs page variants retain loaded reports and expanded rows during background refresh failures, with a small stale-data notice. Initial failures remain visible. An initial budget-load failure shows one safe notice above the tabs, including when Budgets is hidden; successful polling restores incident controls automatically. Company/date changes isolate cached reports. The run-count label is `runs: 0 api · 11 sub`: distinct recorded runs, not tasks or individual model requests. Input totals include cache reads, with the cached portion shown explicitly.
|
||||
|
||||
Receipt identity and supplemental charges: `heartbeat:` idempotency keys are reserved for internally generated run receipts. Additional API-reported charges may link to the same run with their own keys. They contribute to monthly and acknowledged-run lifetime totals, while receipt-integrity checks compare only the original provider receipt. Recovery isolates each budget scope so a deleted target or failed scope does not prevent recovery elsewhere.
|
||||
|
||||
|
||||
@@ -212,12 +212,47 @@ describe.each([
|
||||
expect(container.textContent).not.toContain("0 total events in range");
|
||||
});
|
||||
|
||||
it("retains loaded budget controls and finance totals on background errors", async () => {
|
||||
it("explains first-load budget failures on Overview and restores controls after polling succeeds", async () => {
|
||||
overview.mockRejectedValue(new Error("internal budget stack trace"));
|
||||
await render();
|
||||
const message = "Budget data could not be loaded. Please try again shortly.";
|
||||
const notices = () => [...container.querySelectorAll('[role="status"]')].filter(node => node.textContent === message);
|
||||
expect(notices()).toHaveLength(1);
|
||||
expect(container.querySelector('[role="tab"][data-state="active"]')?.textContent).toBe("Overview");
|
||||
expect(container.textContent).toContain("Bender");
|
||||
expect(container.textContent).not.toContain("Showing the last loaded data");
|
||||
expect(container.textContent).not.toContain("internal budget");
|
||||
expect(container.textContent).not.toContain("Raise budget & resume");
|
||||
await render({ initialTab: "budgets", lockTab: true });
|
||||
expect(container.textContent?.split(message)).toHaveLength(2);
|
||||
expect(container.textContent).not.toContain("Budget control plane");
|
||||
await render();
|
||||
overview.mockResolvedValue({
|
||||
policies: [], activeIncidents: [{ id: "incident", scopeType: "agent", scopeName: "Codie", status: "open", thresholdType: "hard", amountObserved: 200, amountLimit: 1000 }],
|
||||
pendingApprovalCount: 1, pausedAgentCount: 1, pausedProjectCount: 0,
|
||||
});
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(30_000); });
|
||||
await settle();
|
||||
expect(notices()).toHaveLength(0);
|
||||
expect(container.textContent).toContain("Raise budget & resume");
|
||||
expect(container.textContent).toContain("Bender");
|
||||
});
|
||||
|
||||
it("retains loaded budget controls and finance totals on background errors", async () => {
|
||||
overview.mockResolvedValue({
|
||||
policies: [], activeIncidents: [{ id: "incident", scopeType: "agent", scopeName: "Codie", status: "open", thresholdType: "hard", amountObserved: 200, amountLimit: 1000 }],
|
||||
pendingApprovalCount: 1, pausedAgentCount: 1, pausedProjectCount: 0,
|
||||
});
|
||||
await render();
|
||||
const resume = [...container.querySelectorAll("button")].find(node => node.textContent === "Raise budget & resume");
|
||||
expect(resume).toBeDefined();
|
||||
overview.mockRejectedValue(new Error("internal budget failure"));
|
||||
api.financeEvents.mockRejectedValue(new Error("internal finance failure"));
|
||||
await act(async () => { await client.invalidateQueries(); });
|
||||
await settle();
|
||||
expect([...container.querySelectorAll("button")].find(node => node.textContent === "Raise budget & resume")).toBe(resume);
|
||||
expect(container.textContent).toContain("Showing the last loaded data");
|
||||
expect(container.textContent).not.toContain("Budget data could not be loaded");
|
||||
if (name === "streamlined") {
|
||||
await render({ initialTab: "budgets", lockTab: true });
|
||||
expect(container.querySelector('[role="tab"]')).toBeNull();
|
||||
|
||||
@@ -670,9 +670,11 @@ export function Costs({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{((spendData && spendError) || (financeData && financeError) || (budgetData && budgetError) || providerError || billerError || weekError || weekBillerError || windowError) ? (
|
||||
{((spendData && spendError) || (financeData && financeError) || budgetError || providerError || billerError || weekError || weekBillerError || windowError) ? (
|
||||
<p role="status" className="text-sm text-muted-foreground">
|
||||
Showing the last loaded data. Updates will resume automatically.
|
||||
{budgetError && !budgetData
|
||||
? "Budget data could not be loaded. Please try again shortly."
|
||||
: "Showing the last loaded data. Updates will resume automatically."}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -911,9 +913,7 @@ export function Costs({
|
||||
{policyMutation.error && <p role="alert" className="text-sm text-destructive">{policyMutation.error.message}</p>}
|
||||
{budgetLoading ? (
|
||||
<PageSkeleton variant="costs" />
|
||||
) : budgetError && !budgetData ? (
|
||||
<p className="text-sm text-destructive">Budget data could not be loaded. Please try again shortly.</p>
|
||||
) : (
|
||||
) : budgetError && !budgetData ? null : (
|
||||
<>
|
||||
<Card className="border-border/70 bg-(image:--gradient-extract-2)">
|
||||
<CardHeader className="px-5 pt-5 pb-3">
|
||||
|
||||
Reference in new issue
Block a user