// ABOUTME: Unit tests for the training decision engine. // ABOUTME: Tests all 8 priority rules and 4 override scenarios. import { describe, expect, it } from "vitest"; import type { DailyData, OverrideType } from "@/types"; import { getDecisionWithOverrides, getTrainingDecision, } from "./decision-engine"; // Helper to create baseline "healthy" data where TRAIN would be the decision function createHealthyData(): DailyData { return { hrvStatus: "Balanced", bbYesterdayLow: 50, // above 30 phase: "FOLLICULAR", // not LATE_LUTEAL or MENSTRUAL weekIntensity: 0, // well below limit phaseLimit: 120, bbCurrent: 90, // above 85 }; } describe("getTrainingDecision (algorithmic rules)", () => { describe("Priority 1: HRV Unbalanced", () => { it("forces REST when HRV is Unbalanced", () => { const data = createHealthyData(); data.hrvStatus = "Unbalanced"; const result = getTrainingDecision(data); expect(result.status).toBe("REST"); expect(result.reason).toContain("HRV"); }); }); describe("Priority 2: Body Battery Yesterday Low", () => { it("forces REST when BB yesterday low is below 30", () => { const data = createHealthyData(); data.bbYesterdayLow = 25; const result = getTrainingDecision(data); expect(result.status).toBe("REST"); expect(result.reason).toContain("BB"); }); }); describe("Priority 3: Late Luteal Phase", () => { it("forces GENTLE during late luteal phase", () => { const data = createHealthyData(); data.phase = "LATE_LUTEAL"; const result = getTrainingDecision(data); expect(result.status).toBe("GENTLE"); expect(result.reason).toContain("rebounding"); }); }); describe("Priority 4: Menstrual Phase", () => { it("forces GENTLE during menstrual phase", () => { const data = createHealthyData(); data.phase = "MENSTRUAL"; const result = getTrainingDecision(data); expect(result.status).toBe("GENTLE"); expect(result.reason).toContain("rebounding"); }); }); describe("Priority 5: Weekly Intensity Limit", () => { it("forces REST when weekly intensity meets limit", () => { const data = createHealthyData(); data.weekIntensity = 120; // equals phaseLimit const result = getTrainingDecision(data); expect(result.status).toBe("REST"); expect(result.reason).toContain("LIMIT"); }); it("forces REST when weekly intensity exceeds limit", () => { const data = createHealthyData(); data.weekIntensity = 150; // exceeds phaseLimit const result = getTrainingDecision(data); expect(result.status).toBe("REST"); }); }); describe("Priority 6: Body Battery Current Low", () => { it("forces LIGHT when current BB is below 75", () => { const data = createHealthyData(); data.bbCurrent = 70; const result = getTrainingDecision(data); expect(result.status).toBe("LIGHT"); expect(result.reason).toContain("BB"); }); }); describe("Priority 7: Body Battery Current Medium", () => { it("forces REDUCED when current BB is below 85 but above 74", () => { const data = createHealthyData(); data.bbCurrent = 80; const result = getTrainingDecision(data); expect(result.status).toBe("REDUCED"); expect(result.reason).toContain("25%"); }); }); describe("Priority 8: Default", () => { it("returns TRAIN when all conditions are favorable", () => { const data = createHealthyData(); const result = getTrainingDecision(data); expect(result.status).toBe("TRAIN"); expect(result.reason).toContain("OK to train"); }); }); describe("Priority enforcement", () => { it("HRV Unbalanced takes precedence over low BB yesterday", () => { const data = createHealthyData(); data.hrvStatus = "Unbalanced"; data.bbYesterdayLow = 25; const result = getTrainingDecision(data); expect(result.reason).toContain("HRV"); }); it("low BB yesterday takes precedence over phase-based rules", () => { const data = createHealthyData(); data.bbYesterdayLow = 25; data.phase = "LATE_LUTEAL"; const result = getTrainingDecision(data); expect(result.status).toBe("REST"); expect(result.reason).toContain("BB"); }); }); }); describe("getDecisionWithOverrides", () => { describe("override types force REST", () => { it("flare override forces REST", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["flare"]; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); expect(result.reason).toContain("flare"); }); it("stress override forces REST", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["stress"]; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); expect(result.reason).toContain("stress"); }); it("sleep override forces REST", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["sleep"]; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); expect(result.reason).toContain("sleep"); }); it("pms override forces REST", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["pms"]; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); expect(result.reason).toContain("pms"); }); }); describe("override priority (flare > stress > sleep > pms)", () => { it("flare takes precedence over stress", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["stress", "flare"]; const result = getDecisionWithOverrides(data, overrides); expect(result.reason).toContain("flare"); expect(result.reason).not.toContain("stress"); }); it("stress takes precedence over sleep", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["sleep", "stress"]; const result = getDecisionWithOverrides(data, overrides); expect(result.reason).toContain("stress"); expect(result.reason).not.toContain("sleep"); }); it("sleep takes precedence over pms", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["pms", "sleep"]; const result = getDecisionWithOverrides(data, overrides); expect(result.reason).toContain("sleep"); expect(result.reason).not.toContain("pms"); }); it("respects full priority chain with all overrides", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["pms", "sleep", "stress", "flare"]; const result = getDecisionWithOverrides(data, overrides); expect(result.reason).toContain("flare"); }); }); describe("overrides bypass algorithmic rules", () => { it("flare override bypasses favorable conditions", () => { const data = createHealthyData(); // Normally this would return TRAIN const overrides: OverrideType[] = ["flare"]; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); }); it("stress override works even with perfect biometrics", () => { const data = createHealthyData(); data.bbCurrent = 100; data.bbYesterdayLow = 80; const overrides: OverrideType[] = ["stress"]; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); }); }); describe("empty overrides fall through to algorithmic rules", () => { it("returns TRAIN with no overrides and healthy data", () => { const data = createHealthyData(); const overrides: OverrideType[] = []; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("TRAIN"); }); it("returns REST for HRV Unbalanced with no overrides", () => { const data = createHealthyData(); data.hrvStatus = "Unbalanced"; const overrides: OverrideType[] = []; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("REST"); expect(result.reason).toContain("HRV"); }); it("algorithmic rules apply when overrides array is empty", () => { const data = createHealthyData(); data.bbCurrent = 70; const overrides: OverrideType[] = []; const result = getDecisionWithOverrides(data, overrides); expect(result.status).toBe("LIGHT"); }); }); });