From a60ece77c6ce2a073d2f20037f18b54d786fc4a9 Mon Sep 17 00:00:00 2001 From: Molecule AI Frontend Engineer Date: Fri, 17 Apr 2026 02:02:12 +0000 Subject: [PATCH] fix(canvas): use explicit empty-string check in BudgetSection to preserve zero-credit budget parseInt("0", 10) || null evaluates to null, silently converting a zero-credit budget to unlimited. Switch to raw !== "" ? parseInt() : null so budget_limit: 0 is sent correctly. Adds regression test. Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/BudgetSection.test.tsx | 18 ++++++++++++++++++ canvas/src/components/tabs/BudgetSection.tsx | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/canvas/src/components/__tests__/BudgetSection.test.tsx b/canvas/src/components/__tests__/BudgetSection.test.tsx index c9616b06..b0094829 100644 --- a/canvas/src/components/__tests__/BudgetSection.test.tsx +++ b/canvas/src/components/__tests__/BudgetSection.test.tsx @@ -229,6 +229,24 @@ describe("BudgetSection — save", () => { expect(body.budget_limit).toBe(800); }); + it("sends budget_limit: 0 (not null) when input is '0' — zero-credit budget", async () => { + // Regression for QA bug report: `parseInt("0") || null` would yield null. + // The correct form `raw !== "" ? parseInt(raw, 10) : null` must return 0. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockPatch.mockResolvedValueOnce(budgetResponse({ budget_limit: 0, budget_used: 0, budget_remaining: 0 }) as any); + await renderLoaded(budgetResponse({ budget_limit: 1000 })); + + fireEvent.change(screen.getByTestId("budget-limit-input"), { + target: { value: "0" }, + }); + fireEvent.click(screen.getByTestId("budget-save-btn")); + + await waitFor(() => expect(mockPatch).toHaveBeenCalled()); + const body = mockPatch.mock.calls[0][1] as Record; + expect(body.budget_limit).toBe(0); + expect(body.budget_limit).not.toBeNull(); + }); + it("sends budget_limit: null when input is blank", async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any mockPatch.mockResolvedValueOnce(budgetResponse({ budget_limit: null, budget_remaining: null }) as any); diff --git a/canvas/src/components/tabs/BudgetSection.tsx b/canvas/src/components/tabs/BudgetSection.tsx index 86b74daa..24fbe404 100644 --- a/canvas/src/components/tabs/BudgetSection.tsx +++ b/canvas/src/components/tabs/BudgetSection.tsx @@ -80,7 +80,9 @@ export function BudgetSection({ workspaceId }: Props) { setSaving(true); setSaveError(null); const raw = limitInput.trim(); - const parsedLimit = raw ? parseInt(raw, 10) : null; + // Use explicit empty-string check (not falsy check) so that a + // user-entered "0" is sent as budget_limit: 0, not null (unlimited). + const parsedLimit = raw !== "" ? parseInt(raw, 10) : null; try { const updated = await api.patch(`/workspaces/${workspaceId}/budget`, {