import { AllocationStatus } from "@capakraken/shared"; import { describe, expect, it } from "vitest"; import { computeBudgetStatus } from "../budget/monitor.js"; const now = new Date("2025-01-06"); const endDate = new Date("2025-03-07"); const mkAlloc = ( hoursPerDay: number, dailyCostCents: number, status: AllocationStatus = AllocationStatus.CONFIRMED, ) => ({ status, dailyCostCents, hoursPerDay, startDate: now, endDate, }); describe("computeBudgetStatus", () => { it("returns 0% utilization for no allocations", () => { const result = computeBudgetStatus(1000000, 100, [], now, endDate); expect(result.allocatedCents).toBe(0); expect(result.utilizationPercent).toBe(0); expect(result.remainingCents).toBe(1000000); expect(result.warnings).toHaveLength(0); }); it("calculates correct utilization", () => { // 1 alloc, 10 working days, 8000 cents/day = 80000 cents const alloc = { ...mkAlloc(8, 8000), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-17") }; const result = computeBudgetStatus(400000, 100, [alloc], now, endDate); expect(result.confirmedCents).toBe(80000); expect(result.utilizationPercent).toBeCloseTo(20, 0); }); it("emits WARNING at 85% utilization", () => { // Budget 100000, alloc cost 87000 = 87% const alloc = { ...mkAlloc(8, 87000 / 10), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-17") }; const result = computeBudgetStatus(100000, 100, [alloc], now, endDate); const hasWarning = result.warnings.some((w) => w.code === "BUDGET_WARNING"); expect(hasWarning).toBe(true); }); it("emits CRITICAL at 95% utilization", () => { const alloc = { ...mkAlloc(8, 96000 / 10), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-17") }; const result = computeBudgetStatus(100000, 100, [alloc], now, endDate); const hasCritical = result.warnings.some((w) => w.code === "BUDGET_CRITICAL"); expect(hasCritical).toBe(true); }); it("emits EXCEEDED when allocations exceed budget", () => { const alloc = { ...mkAlloc(8, 120000 / 10), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-17") }; const result = computeBudgetStatus(100000, 100, [alloc], now, endDate); const hasExceeded = result.warnings.some((w) => w.code === "BUDGET_EXCEEDED"); expect(hasExceeded).toBe(true); }); it("applies win probability weighting", () => { const alloc = { ...mkAlloc(8, 10000), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-17") }; const result = computeBudgetStatus(1000000, 50, [alloc], now, endDate); // allocated = 10 days × 10000 = 100000, weighted = 50% = 50000 expect(result.winProbabilityWeightedCents).toBe(Math.round(result.allocatedCents * 0.5)); }); it("separates proposed from confirmed costs", () => { const confirmed = { ...mkAlloc(8, 5000, AllocationStatus.CONFIRMED), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-10") }; const proposed = { ...mkAlloc(8, 5000, AllocationStatus.PROPOSED), startDate: new Date("2025-01-06"), endDate: new Date("2025-01-10") }; const result = computeBudgetStatus(1000000, 100, [confirmed, proposed], now, endDate); expect(result.confirmedCents).toBeGreaterThan(0); expect(result.proposedCents).toBeGreaterThan(0); expect(result.confirmedCents).toBe(result.proposedCents); }); });