Fix critical bug: cycle phase boundaries now scale with cycle length

CRITICAL BUG FIX:
- Phase boundaries were hardcoded for 31-day cycle, breaking correct
  phase calculations for users with different cycle lengths (28, 35, etc.)
- Added getPhaseBoundaries(cycleLength) function in cycle.ts
- Updated getPhase() to accept cycleLength parameter (default 31)
- Updated all callers (API routes, components) to pass cycleLength
- Added 13 new tests for phase boundaries with 28, 31, and 35-day cycles

ICS IMPROVEMENTS:
- Fixed emojis to match calendar.md spec: 🩸🌱🌸🌙🌑
- Added CATEGORIES field for calendar app colors per spec:
  MENSTRUAL=Red, FOLLICULAR=Green, OVULATION=Pink,
  EARLY_LUTEAL=Yellow, LATE_LUTEAL=Orange
- Added 5 new tests for CATEGORIES

Updated IMPLEMENTATION_PLAN.md with discovered issues and test counts.

825 tests passing (up from 807)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 22:39:09 +00:00
parent 58f6c5605a
commit a977934c23
15 changed files with 337 additions and 148 deletions

View File

@@ -1,5 +1,5 @@
// ABOUTME: Unit tests for cycle phase calculation utilities.
// ABOUTME: Tests getCycleDay, getPhase, and phase limit functions.
// ABOUTME: Tests getCycleDay, getPhase, and phase limit functions with variable cycle lengths.
import { describe, expect, it } from "vitest";
import { getCycleDay, getPhase, getPhaseLimit } from "./cycle";
@@ -25,29 +25,133 @@ describe("getCycleDay", () => {
});
describe("getPhase", () => {
it("returns MENSTRUAL for days 1-3", () => {
expect(getPhase(1)).toBe("MENSTRUAL");
expect(getPhase(3)).toBe("MENSTRUAL");
// Phase boundaries per spec (cycle-tracking.md):
// MENSTRUAL: 1-3 (fixed)
// FOLLICULAR: 4 to (cycleLength - 16)
// OVULATION: (cycleLength - 15) to (cycleLength - 14)
// EARLY_LUTEAL: (cycleLength - 13) to (cycleLength - 7)
// LATE_LUTEAL: (cycleLength - 6) to cycleLength
describe("31-day cycle", () => {
const cycleLength = 31;
it("returns MENSTRUAL for days 1-3", () => {
expect(getPhase(1, cycleLength)).toBe("MENSTRUAL");
expect(getPhase(3, cycleLength)).toBe("MENSTRUAL");
});
it("returns FOLLICULAR for days 4-15", () => {
// 4 to (31-16) = 4-15
expect(getPhase(4, cycleLength)).toBe("FOLLICULAR");
expect(getPhase(15, cycleLength)).toBe("FOLLICULAR");
});
it("returns OVULATION for days 16-17", () => {
// (31-15) to (31-14) = 16-17
expect(getPhase(16, cycleLength)).toBe("OVULATION");
expect(getPhase(17, cycleLength)).toBe("OVULATION");
});
it("returns EARLY_LUTEAL for days 18-24", () => {
// (31-13) to (31-7) = 18-24
expect(getPhase(18, cycleLength)).toBe("EARLY_LUTEAL");
expect(getPhase(24, cycleLength)).toBe("EARLY_LUTEAL");
});
it("returns LATE_LUTEAL for days 25-31", () => {
// (31-6) to 31 = 25-31
expect(getPhase(25, cycleLength)).toBe("LATE_LUTEAL");
expect(getPhase(31, cycleLength)).toBe("LATE_LUTEAL");
});
});
it("returns FOLLICULAR for days 4-14", () => {
expect(getPhase(4)).toBe("FOLLICULAR");
expect(getPhase(14)).toBe("FOLLICULAR");
describe("28-day cycle", () => {
const cycleLength = 28;
it("returns MENSTRUAL for days 1-3", () => {
expect(getPhase(1, cycleLength)).toBe("MENSTRUAL");
expect(getPhase(3, cycleLength)).toBe("MENSTRUAL");
});
it("returns FOLLICULAR for days 4-12", () => {
// 4 to (28-16) = 4-12
expect(getPhase(4, cycleLength)).toBe("FOLLICULAR");
expect(getPhase(12, cycleLength)).toBe("FOLLICULAR");
});
it("returns OVULATION for days 13-14", () => {
// (28-15) to (28-14) = 13-14
expect(getPhase(13, cycleLength)).toBe("OVULATION");
expect(getPhase(14, cycleLength)).toBe("OVULATION");
});
it("returns EARLY_LUTEAL for days 15-21", () => {
// (28-13) to (28-7) = 15-21
expect(getPhase(15, cycleLength)).toBe("EARLY_LUTEAL");
expect(getPhase(21, cycleLength)).toBe("EARLY_LUTEAL");
});
it("returns LATE_LUTEAL for days 22-28", () => {
// (28-6) to 28 = 22-28
expect(getPhase(22, cycleLength)).toBe("LATE_LUTEAL");
expect(getPhase(28, cycleLength)).toBe("LATE_LUTEAL");
});
});
it("returns OVULATION for days 15-16", () => {
expect(getPhase(15)).toBe("OVULATION");
expect(getPhase(16)).toBe("OVULATION");
describe("35-day cycle", () => {
const cycleLength = 35;
it("returns MENSTRUAL for days 1-3", () => {
expect(getPhase(1, cycleLength)).toBe("MENSTRUAL");
expect(getPhase(3, cycleLength)).toBe("MENSTRUAL");
});
it("returns FOLLICULAR for days 4-19", () => {
// 4 to (35-16) = 4-19
expect(getPhase(4, cycleLength)).toBe("FOLLICULAR");
expect(getPhase(19, cycleLength)).toBe("FOLLICULAR");
});
it("returns OVULATION for days 20-21", () => {
// (35-15) to (35-14) = 20-21
expect(getPhase(20, cycleLength)).toBe("OVULATION");
expect(getPhase(21, cycleLength)).toBe("OVULATION");
});
it("returns EARLY_LUTEAL for days 22-28", () => {
// (35-13) to (35-7) = 22-28
expect(getPhase(22, cycleLength)).toBe("EARLY_LUTEAL");
expect(getPhase(28, cycleLength)).toBe("EARLY_LUTEAL");
});
it("returns LATE_LUTEAL for days 29-35", () => {
// (35-6) to 35 = 29-35
expect(getPhase(29, cycleLength)).toBe("LATE_LUTEAL");
expect(getPhase(35, cycleLength)).toBe("LATE_LUTEAL");
});
});
it("returns EARLY_LUTEAL for days 17-24", () => {
expect(getPhase(17)).toBe("EARLY_LUTEAL");
expect(getPhase(24)).toBe("EARLY_LUTEAL");
});
describe("edge cases", () => {
it("defaults to LATE_LUTEAL for days beyond cycle length", () => {
expect(getPhase(32, 31)).toBe("LATE_LUTEAL");
expect(getPhase(40, 35)).toBe("LATE_LUTEAL");
});
it("returns LATE_LUTEAL for days 25-31", () => {
expect(getPhase(25)).toBe("LATE_LUTEAL");
expect(getPhase(31)).toBe("LATE_LUTEAL");
it("handles minimum cycle length (21 days)", () => {
// 21-day: FOLLICULAR 4-5, OVULATION 6-7, EARLY_LUTEAL 8-14, LATE_LUTEAL 15-21
expect(getPhase(5, 21)).toBe("FOLLICULAR"); // 4 to (21-16)=5
expect(getPhase(6, 21)).toBe("OVULATION"); // (21-15)=6 to (21-14)=7
expect(getPhase(8, 21)).toBe("EARLY_LUTEAL"); // (21-13)=8 to (21-7)=14
expect(getPhase(15, 21)).toBe("LATE_LUTEAL"); // (21-6)=15 to 21
});
it("handles maximum cycle length (45 days)", () => {
// 45-day: FOLLICULAR 4-29, OVULATION 30-31, EARLY_LUTEAL 32-38, LATE_LUTEAL 39-45
expect(getPhase(29, 45)).toBe("FOLLICULAR"); // 4 to (45-16)=29
expect(getPhase(30, 45)).toBe("OVULATION"); // (45-15)=30 to (45-14)=31
expect(getPhase(32, 45)).toBe("EARLY_LUTEAL"); // (45-13)=32 to (45-7)=38
expect(getPhase(39, 45)).toBe("LATE_LUTEAL"); // (45-6)=39 to 45
});
});
});