diff --git a/src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard.tsx b/src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard.tsx index 6bae71c651..bd131e0841 100644 --- a/src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard.tsx +++ b/src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard.tsx @@ -24,8 +24,9 @@ export interface PoolCardProps { } function computeStatus(usage: PoolUsageSnapshot | null): "green" | "amber" | "red" { - if (!usage || usage.dimensions.length === 0) return "green"; - const utilizations = usage.dimensions.map((d) => + const dims = usage?.dimensions ?? []; + if (dims.length === 0) return "green"; + const utilizations = dims.map((d) => d.limit > 0 ? (d.consumedTotal / d.limit) * 100 : 0 ); const avg = utilizations.reduce((s, u) => s + u, 0) / utilizations.length; @@ -54,7 +55,7 @@ export default function PoolCard({ const { icon: statusIcon, cls: statusCls } = STATUS_ICONS[status]; // Check for plan dimensions from usage - const hasDimensions = usage && usage.dimensions.length > 0; + const hasDimensions = !!usage?.dimensions?.length; return ( @@ -103,10 +104,10 @@ export default function PoolCard({
- {usage.dimensions.map((dim, i) => ( + {(usage?.dimensions ?? []).map((dim, i) => ( 0) { totalUtil += (dim.consumedTotal / dim.limit) * 100; utilCount += 1; } - for (const key of dim.perKey) { + for (const key of dim.perKey ?? []) { if (key.borrowing) borrowing += 1; } } diff --git a/tests/unit/v388-quota-share-usage-guard.test.ts b/tests/unit/v388-quota-share-usage-guard.test.ts new file mode 100644 index 0000000000..206379e22c --- /dev/null +++ b/tests/unit/v388-quota-share-usage-guard.test.ts @@ -0,0 +1,23 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +// Regression guard: quota-share crashed with "Cannot read properties of undefined +// (reading 'length')" because PoolCard/aggregate read usage.dimensions without a +// guard when the usage snapshot came back without a dimensions array. +const root = join(import.meta.dirname, "../.."); +const read = (p: string) => readFileSync(join(root, p), "utf8"); + +test("quota-share PoolCard guards usage.dimensions", () => { + const pc = read("src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard.tsx"); + assert.ok(pc.includes("usage?.dimensions ?? []"), "computeStatus normalizes dimensions to []"); + assert.ok(pc.includes("!!usage?.dimensions?.length"), "hasDimensions guards dimensions"); + assert.equal(/[^?.]usage\.dimensions\.length/.test(pc), false, "no unguarded usage.dimensions.length"); +}); + +test("quota-share aggregate hook guards dimensions/perKey", () => { + const agg = read("src/app/(dashboard)/dashboard/costs/quota-share/hooks/usePoolsUsageAggregate.ts"); + assert.ok(agg.includes("usage.dimensions ?? []"), "iterates dimensions with ?? []"); + assert.ok(agg.includes("dim.perKey ?? []"), "iterates perKey with ?? []"); +});