1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
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),
);
});
});
|