diff options
Diffstat (limited to 'src/lib/shared/routine-metrics.test.ts')
| -rw-r--r-- | src/lib/shared/routine-metrics.test.ts | 116 |
1 files changed, 62 insertions, 54 deletions
diff --git a/src/lib/shared/routine-metrics.test.ts b/src/lib/shared/routine-metrics.test.ts index d18fd18..b3b84a2 100644 --- a/src/lib/shared/routine-metrics.test.ts +++ b/src/lib/shared/routine-metrics.test.ts @@ -6,9 +6,19 @@ import { computeProjectHealthSeries, parseIntervalToDays, } from './routine-metrics'; - -const DAY_MS = 24 * 60 * 60 * 1000; const CREATED = '2026-09-01T00:00:00.000Z'; +const created = Temporal.Instant.from(CREATED); + +const plusDays = (base: Temporal.Instant, days: number): Temporal.Instant => + base.toZonedDateTimeISO('UTC').add({ days }).toInstant(); + +/** ISO stamp n days after the given stamp. */ +const isoPlusDays = (iso: string, days: number): string => + Temporal.Instant.from(iso) + .toZonedDateTimeISO('UTC') + .add({ days }) + .toInstant() + .toString({ smallestUnit: 'millisecond' }); const routine = { id: 1, @@ -20,33 +30,31 @@ const routine = { describe('computeRoutineState', () => { it('is due when never done, even before the due date', () => { - const now = Date.parse(CREATED) + 3 * DAY_MS; + const now = plusDays(created, 3); const state = computeRoutineState(routine, [], now); expect(state.status).toBe('due'); expect(state.overdueDays).toBe(0); expect(state.health).toBe(100); - expect(state.dueAt).toBe( - new Date(Date.parse(CREATED) + 7 * DAY_MS).toISOString(), - ); + expect(state.dueAt).toBe(isoPlusDays(CREATED, 7)); }); 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 doneAt = isoPlusDays(CREATED, 5); + const now = plusDays(Temporal.Instant.from(doneAt), 2); 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 now = plusDays(created, 7); const state = computeRoutineState(routine, [], now); expect(state.status).toBe('due'); expect(state.health).toBe(100); }); it('is very urgent (veryLate) when two full periods overdue', () => { - const now = Date.parse(CREATED) + 22 * DAY_MS; + const now = plusDays(created, 22); const state = computeRoutineState(routine, [], now); expect(state.status).toBe('veryLate'); expect(state.overdueDays).toBe(15); @@ -54,27 +62,31 @@ describe('computeRoutineState', () => { }); it('is due (not late) when overdue by half a frequency', () => { - const now = Date.parse(CREATED) + 10.5 * DAY_MS; + const now = created + .toZonedDateTimeISO('UTC') + .add({ days: 10 }) + .toInstant() + .add({ hours: 12 }); const state = computeRoutineState(routine, [], now); expect(state.status).toBe('due'); }); it('is late when a full period overdue', () => { - const now = Date.parse(CREATED) + 14 * DAY_MS; + const now = plusDays(created, 14); const state = computeRoutineState(routine, [], now); expect(state.status).toBe('late'); expect(state.health).toBe(50); }); it('health of 50 means a full period overdue', () => { - const now = Date.parse(CREATED) + 7 * DAY_MS + 7 * DAY_MS; + const now = plusDays(created, 14); const state = computeRoutineState(routine, [], now); expect(state.health).toBe(50); }); it('is due the calendar day after completion for a daily routine', () => { const daily = { ...routine, frequencyDays: 1 }; const doneAt = '2026-09-07T20:00:00.000Z'; // yesterday evening - const now = Date.parse('2026-09-08T08:00:00.000Z'); // this morning, <24h later + const now = Temporal.Instant.from('2026-09-08T08:00:00.000Z'); // this morning, <24h later const state = computeRoutineState(daily, [{ routineId: 1, doneAt }], now); expect(state.status).toBe('due'); }); @@ -86,13 +98,13 @@ describe('computeRoutineState', () => { const today = computeRoutineState( daily, completions, - Date.parse('2026-09-08T20:00:00.000Z'), + Temporal.Instant.from('2026-09-08T20:00:00.000Z'), ); expect(today.status).toBe('due'); const tomorrow = computeRoutineState( daily, completions, - Date.parse('2026-09-09T20:00:00.000Z'), + Temporal.Instant.from('2026-09-09T20:00:00.000Z'), ); expect(tomorrow.status).toBe('late'); }); @@ -100,7 +112,7 @@ describe('computeRoutineState', () => { it('is ok later the same day it was done for a daily routine', () => { const daily = { ...routine, frequencyDays: 1 }; const doneAt = '2026-09-08T08:00:00.000Z'; - const now = Date.parse('2026-09-08T20:00:00.000Z'); + const now = Temporal.Instant.from('2026-09-08T20:00:00.000Z'); const state = computeRoutineState(daily, [{ routineId: 1, doneAt }], now); expect(state.status).toBe('ok'); }); @@ -108,40 +120,47 @@ describe('computeRoutineState', () => { describe('computeHealthSeries', () => { it('decays from 100 at creation to 0 two periods overdue when never done', () => { - const now = Date.parse(CREATED) + 22 * DAY_MS; + const now = plusDays(created, 22); const series = computeHealthSeries(routine, [], now); expect(series[0].health).toBe(100); - const at8d = series.find((p) => p.at === Date.parse(CREATED) + 8 * DAY_MS); + const at8d = series.find( + (p) => p.at === plusDays(created, 8).epochMilliseconds, + ); expect(at8d?.health).toBe(93); // 1/7 of a period overdue const last = series[series.length - 1]; - expect(last.at).toBe(now); + expect(last.at).toBe(now.epochMilliseconds); expect(last.health).toBe(0); // more than two full periods overdue }); it('resets to 100 right after a completion, then decays again (sawtooth)', () => { - const doneAt = Date.parse(CREATED) + 5 * DAY_MS; - const now = doneAt + 13 * DAY_MS; // 13 days after the completion + const doneAt = plusDays(created, 5); + const now = plusDays(doneAt, 13); // 13 days after the completion + const doneAtIso = doneAt.toString({ smallestUnit: 'millisecond' }); const series = computeHealthSeries( routine, - [{ routineId: 1, doneAt: new Date(doneAt).toISOString() }], + [{ routineId: 1, doneAt: doneAtIso }], now, ); - const rightAfter = series.find((p) => p.at >= doneAt); + const rightAfter = series.find((p) => p.at >= doneAt.epochMilliseconds); expect(rightAfter?.health).toBe(100); // 8 days after the completion: 1 day into the 7-day period overdue. - const at8d = series.find((p) => p.at === doneAt + 8 * DAY_MS); + const at8d = series.find( + (p) => p.at === plusDays(doneAt, 8).epochMilliseconds, + ); expect(at8d?.health).toBe(93); }); it('a second completion resets the decay again', () => { - const first = Date.parse(CREATED) + 3 * DAY_MS; - const second = Date.parse(CREATED) + 10 * DAY_MS; - const now = Date.parse(CREATED) + 11 * DAY_MS; + const first = plusDays(created, 3); + const second = plusDays(created, 10); + const now = plusDays(created, 11); + const stamp = (at: Temporal.Instant): string => + at.toString({ smallestUnit: 'millisecond' }); const series = computeHealthSeries( routine, [ - { routineId: 1, doneAt: new Date(first).toISOString() }, - { routineId: 1, doneAt: new Date(second).toISOString() }, + { routineId: 1, doneAt: stamp(first) }, + { routineId: 1, doneAt: stamp(second) }, ], now, ); @@ -153,28 +172,25 @@ describe('computeHealthSeries', () => { describe('computeProjectHealthSeries', () => { it('is empty with no routines', () => { - expect(computeProjectHealthSeries([], [], Date.parse(CREATED))).toEqual([]); + expect(computeProjectHealthSeries([], [], created)).toEqual([]); }); it('averages per-routine health and ends at the current score', () => { const second = { ...routine, id: 2, name: 'read' }; - const doneAt = Date.parse(CREATED) + 5 * DAY_MS; - const now = Date.parse(CREATED) + 8 * DAY_MS; + const doneAt = plusDays(created, 5); + const now = plusDays(created, 8); + const doneAtIso = doneAt.toString({ smallestUnit: 'millisecond' }); const series = computeProjectHealthSeries( [routine, second], - [{ routineId: 2, doneAt: new Date(doneAt).toISOString() }], + [{ routineId: 2, doneAt: doneAtIso }], now, ); const states = [ computeRoutineState(routine, [], now), - computeRoutineState( - second, - [{ routineId: 2, doneAt: new Date(doneAt).toISOString() }], - now, - ), + computeRoutineState(second, [{ routineId: 2, doneAt: doneAtIso }], now), ]; const last = series[series.length - 1]; - expect(last.at).toBe(now); + expect(last.at).toBe(now.epochMilliseconds); expect(last.health).toBe(computeHealthScore(states)); }); @@ -183,18 +199,14 @@ describe('computeProjectHealthSeries', () => { ...routine, id: 2, name: 'latecomer', - createdAt: new Date(Date.parse(CREATED) + 10 * DAY_MS).toISOString(), + createdAt: isoPlusDays(CREATED, 10), }; - const now = Date.parse(CREATED) + 11 * DAY_MS; + const now = plusDays(created, 11); const series = computeProjectHealthSeries([routine, late], [], now); const beforeLate = series.find( - (p) => p.at === Date.parse(CREATED) + 5 * DAY_MS, - ); - const single = computeHealthSeries( - routine, - [], - Date.parse(CREATED) + 5 * DAY_MS, + (p) => p.at === plusDays(created, 5).epochMilliseconds, ); + const single = computeHealthSeries(routine, [], plusDays(created, 5)); expect(beforeLate?.health).toBe(single[single.length - 1].health); }); }); @@ -226,13 +238,9 @@ describe('computeHealthScore', () => { }); it('averages routine healths', () => { - const now = Date.parse(CREATED) + 8 * DAY_MS; + const now = plusDays(created, 8); const a = computeRoutineState(routine, [], now); // overdue 1 day - const b = computeRoutineState( - routine, - [], - Date.parse(CREATED) + 3 * DAY_MS, - ); + const b = computeRoutineState(routine, [], plusDays(created, 3)); expect(computeHealthScore([a, b])).toBe( Math.round((a.health + b.health) / 2), ); |
