aboutsummaryrefslogtreecommitdiff
path: root/src/lib/shared/routine-metrics.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/shared/routine-metrics.test.ts')
-rw-r--r--src/lib/shared/routine-metrics.test.ts116
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),
);