import { describe, expect, it } from "vitest"; import { updateCookingAssumptionProfile, rollbackCookingAssumptionProfile, } from "../src/cooking-profiles.js"; describe("updateCookingAssumptionProfile", () => { it("initializes profile from first observation", () => { const result = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: 3, leftoverEstimatePortions: 1, sessionId: "s1", date: "2026-08-07", }, { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 }, ); expect(result.averageEatenPortions).toBe(3); expect(result.averageLeftoverPortions).toBe(1); expect(result.observationCount).toBe(1); }); it("clamps eaten and leftovers to valid ranges", () => { const result = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: -1, leftoverEstimatePortions: 5, sessionId: "s1", date: "2026-08-07", }, { averageEatenPortions: 2, averageLeftoverPortions: 1, observationCount: 3 }, ); // Eaten clamps to 0, leftovers clamps to planned - eaten = 4. // alpha = 1/4, prevEaten=2 => 2 + 0.25*(0-2) = 1.5 // prevLeftovers=1 => 1 + 0.25*(4-1) = 1.75 expect(result.averageEatenPortions).toBe(1.5); expect(result.averageLeftoverPortions).toBe(1.75); }); it("keeps a rolling history of last 10 sessions", () => { type Profile = { averageEatenPortions: number | null; averageLeftoverPortions: number | null; observationCount: number; lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null }; let existing: Profile = { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 }; for (let i = 0; i < 12; i++) { existing = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: 2, leftoverEstimatePortions: 1, sessionId: `s${i}`, date: "2026-08-07", }, existing, ); } expect(existing.lastSessionAnswers).toHaveLength(10); expect(existing.lastSessionAnswers![0]!.sessionId).toBe("s11"); }); }); describe("rollbackCookingAssumptionProfile", () => { it("returns null averages and zero count when history becomes empty", () => { const existing = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: 3, leftoverEstimatePortions: 1, sessionId: "s1", date: "2026-08-07", }, { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 }, ); const rolled = rollbackCookingAssumptionProfile("s1", existing); expect(rolled.averageEatenPortions).toBeNull(); expect(rolled.averageLeftoverPortions).toBeNull(); expect(rolled.observationCount).toBe(0); expect(rolled.lastSessionAnswers).toHaveLength(0); }); it("recalculates averages deterministically from remaining history", () => { let existing: { averageEatenPortions: number | null; averageLeftoverPortions: number | null; observationCount: number; lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null; } = { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 }; // Three chronological observations. const observations = [ { sessionId: "s1", eaten: 4, leftovers: 0 }, { sessionId: "s2", eaten: 2, leftovers: 1 }, { sessionId: "s3", eaten: 3, leftovers: 1 }, ]; for (const obs of observations) { existing = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: obs.eaten, leftoverEstimatePortions: obs.leftovers, sessionId: obs.sessionId, date: "2026-08-07", }, existing, ); } // Roll back the middle observation and recompute from scratch. const rolled = rollbackCookingAssumptionProfile("s2", existing); expect(rolled.lastSessionAnswers).toHaveLength(2); expect(rolled.lastSessionAnswers!.map((a) => a.sessionId)).toContain("s1"); expect(rolled.lastSessionAnswers!.map((a) => a.sessionId)).toContain("s3"); // Recompute EMA manually from s1 then s3. // s1: avgEaten=4, avgLeftovers=0 // s3: alpha=1/2 => eaten=4+0.5*(3-4)=3.5, leftovers=0+0.5*(1-0)=0.5 expect(rolled.averageEatenPortions).toBe(3.5); expect(rolled.averageLeftoverPortions).toBe(0.5); expect(rolled.observationCount).toBe(2); }); it("is idempotent: rolling back the same session twice leaves history unchanged", () => { let existing: { averageEatenPortions: number | null; averageLeftoverPortions: number | null; observationCount: number; lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null; } = { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 }; existing = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: 3, leftoverEstimatePortions: 1, sessionId: "s1", date: "2026-08-07", }, existing, ); existing = updateCookingAssumptionProfile( { householdId: "h1", canonicalIngredientId: "i1", plannedPortions: 4, actualPortionsEaten: 2, leftoverEstimatePortions: 0, sessionId: "s2", date: "2026-08-07", }, existing, ); const first = rollbackCookingAssumptionProfile("s1", existing); const second = rollbackCookingAssumptionProfile("s1", first); expect(second).toEqual(first); }); });