functional
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { estimate, totalActiveDays, activeDaysConsumed } from "./engine";
|
||||
import type { Config, Selections } from "./types";
|
||||
import { parseISODate } from "./dates";
|
||||
|
||||
function d(iso: string): Date {
|
||||
const parsed = parseISODate(iso);
|
||||
if (!parsed) throw new Error(`bad test date ${iso}`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// A simple 100-day window (Jan 1 .. Apr 10, 2026 is 100 inclusive days).
|
||||
const config: Config = {
|
||||
mealStartDate: d("2026-01-01"),
|
||||
mealEndDate: d("2026-04-10"),
|
||||
plans: [
|
||||
{ id: "green", displayName: "Green", totalMealBlocks: 200, totalFlexDollars: 500 },
|
||||
],
|
||||
missingPeriods: [
|
||||
{ id: "break", displayName: "Break", startDate: d("2026-02-01"), endDate: d("2026-02-10") }, // 10 days
|
||||
],
|
||||
};
|
||||
|
||||
function sel(overrides: Partial<Selections> = {}): Selections {
|
||||
return {
|
||||
selectedPlanId: "green",
|
||||
targetDate: d("2026-04-10"),
|
||||
activeMissingPeriodIds: new Set(),
|
||||
extraExcludedDays: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("totalActiveDays", () => {
|
||||
it("counts the full inclusive window with no missing periods", () => {
|
||||
expect(totalActiveDays(config, new Set())).toBe(100);
|
||||
});
|
||||
|
||||
it("subtracts an active missing period", () => {
|
||||
expect(totalActiveDays(config, new Set(["break"]))).toBe(90);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activeDaysConsumed", () => {
|
||||
it("is the full window at the end date", () => {
|
||||
expect(activeDaysConsumed(config, sel())).toBe(100);
|
||||
});
|
||||
|
||||
it("clamps a target date before the start to zero", () => {
|
||||
expect(activeDaysConsumed(config, sel({ targetDate: d("2025-12-01") }))).toBe(0);
|
||||
});
|
||||
|
||||
it("subtracts extra excluded days", () => {
|
||||
expect(activeDaysConsumed(config, sel({ extraExcludedDays: 5 }))).toBe(95);
|
||||
});
|
||||
});
|
||||
|
||||
describe("estimate", () => {
|
||||
it("conserves the budget at the end date with no exclusions", () => {
|
||||
const r = estimate(config, sel());
|
||||
expect(r.consumedMealBlocks).toBeCloseTo(200);
|
||||
expect(r.remainingMealBlocks).toBeCloseTo(0);
|
||||
expect(r.consumedFlexDollars).toBeCloseTo(500);
|
||||
expect(r.remainingFlexDollars).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it("consumed + remaining always equals the plan total", () => {
|
||||
const r = estimate(config, sel({ targetDate: d("2026-02-15"), activeMissingPeriodIds: new Set(["break"]) }));
|
||||
expect(r.consumedMealBlocks + r.remainingMealBlocks).toBeCloseTo(200);
|
||||
expect(r.consumedFlexDollars + r.remainingFlexDollars).toBeCloseTo(500);
|
||||
});
|
||||
|
||||
it("computes a per-week rate of dailyRate * 7", () => {
|
||||
const r = estimate(config, sel());
|
||||
// 200 blocks / 100 days = 2/day => 14/week
|
||||
expect(r.perWeekMealBlocks).toBeCloseTo(14);
|
||||
expect(r.perWeekFlexDollars).toBeCloseTo(35); // 500/100*7
|
||||
});
|
||||
|
||||
it("never lets excluding days increase consumption (monotonicity)", () => {
|
||||
const base = estimate(config, sel({ targetDate: d("2026-03-01") }));
|
||||
const more = estimate(config, sel({ targetDate: d("2026-03-01"), extraExcludedDays: 10 }));
|
||||
expect(more.consumedMealBlocks).toBeLessThanOrEqual(base.consumedMealBlocks);
|
||||
});
|
||||
|
||||
it("handles zero active days without dividing by zero", () => {
|
||||
const allExcluded: Config = {
|
||||
...config,
|
||||
missingPeriods: [
|
||||
{ id: "all", displayName: "All", startDate: d("2026-01-01"), endDate: d("2026-04-10") },
|
||||
],
|
||||
};
|
||||
const r = estimate(allExcluded, sel({ activeMissingPeriodIds: new Set(["all"]) }));
|
||||
expect(r.perWeekMealBlocks).toBe(0);
|
||||
expect(r.consumedMealBlocks).toBe(0);
|
||||
expect(Number.isFinite(r.remainingMealBlocks)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user