import { describe, it, expect } from 'vitest'; import { computeRoutineState, computeHealthScore, parseIntervalToDays, } from './routine-metrics'; const DAY_MS = 24 * 60 * 60 * 1000; const CREATED = '2026-09-01T00:00:00.000Z'; const routine = { id: 1, version: 1, name: 'clean kitchen', frequencyDays: 7, createdAt: CREATED, }; describe('computeRoutineState', () => { it('is ok and healthy before the due date', () => { const now = Date.parse(CREATED) + 3 * DAY_MS; const state = computeRoutineState(routine, [], now); expect(state.status).toBe('ok'); expect(state.overdueDays).toBe(0); expect(state.health).toBe(100); expect(state.dueAt).toBe( new Date(Date.parse(CREATED) + 7 * DAY_MS).toISOString(), ); }); it('uses the last completion as reference once done', () => { const doneAt = new Date(Date.parse(CREATED) + 5 * DAY_MS).toISOString(); const now = Date.parse(doneAt) + 2 * DAY_MS; const state = computeRoutineState(routine, [{ routineId: 1, doneAt }], now); expect(state.lastDoneAt).toBe(doneAt); expect(state.status).toBe('ok'); }); it('is due at exactly the frequency', () => { const now = Date.parse(CREATED) + 7 * DAY_MS; const state = computeRoutineState(routine, [], now); expect(state.status).toBe('due'); expect(state.health).toBe(100); }); it('is very urgent (veryLate) when two weeks old for a weekly routine', () => { const now = Date.parse(CREATED) + 15 * DAY_MS; const state = computeRoutineState(routine, [], now); expect(state.status).toBe('veryLate'); expect(state.overdueDays).toBe(8); expect(state.health).toBe(0); }); it('is late when overdue by half a frequency', () => { const now = Date.parse(CREATED) + 10.5 * DAY_MS; const state = computeRoutineState(routine, [], now); expect(state.status).toBe('late'); }); it('health of 75 means a quarter of the period overdue', () => { const now = Date.parse(CREATED) + 7 * DAY_MS + 1.75 * DAY_MS; const state = computeRoutineState(routine, [], now); expect(state.health).toBe(75); }); }); describe('parseIntervalToDays', () => { it('converts units to days', () => { expect(parseIntervalToDays('1d')).toBe(1); expect(parseIntervalToDays('2w')).toBe(14); expect(parseIntervalToDays('3m')).toBe(90); expect(parseIntervalToDays('1y')).toBe(365); }); it('accepts whitespace and uppercase', () => { expect(parseIntervalToDays(' 2W ')).toBe(14); }); it('rejects invalid input', () => { expect(parseIntervalToDays('2')).toBeNull(); expect(parseIntervalToDays('w')).toBeNull(); expect(parseIntervalToDays('2x')).toBeNull(); expect(parseIntervalToDays('-1d')).toBeNull(); expect(parseIntervalToDays('')).toBeNull(); }); }); describe('computeHealthScore', () => { it('is 100 with no routines', () => { expect(computeHealthScore([])).toBe(100); }); it('averages routine healths', () => { const now = Date.parse(CREATED) + 8 * DAY_MS; const a = computeRoutineState(routine, [], now); // overdue 1 day const b = computeRoutineState( routine, [], Date.parse(CREATED) + 3 * DAY_MS, ); expect(computeHealthScore([a, b])).toBe( Math.round((a.health + b.health) / 2), ); }); });