summaryrefslogtreecommitdiffhomepage
path: root/packages/api/tests/wake-scheduler.test.ts
blob: 0e5731c532389b953515db28fe1ccb339fa3fe11 (plain)
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
import { describe, expect, it } from "vitest";
import {
	CLAUDE_RESET_OFFSET_HOURS,
	DAILY_INTERVAL_MS,
	MISSED_WAKE_GRACE_MS,
	nextDailyAfter,
	recoverScheduleEntry,
	resetHourFor,
} from "../src/wake-scheduler.js";

const HOUR = 60 * 60 * 1000;

describe("nextDailyAfter", () => {
	it("returns the input when it is already strictly in the future", () => {
		const now = 1_700_000_000_000;
		const future = now + 60_000;
		expect(nextDailyAfter(future, now)).toBe(future);
	});

	it("advances by exactly 24h when the input is 1ms in the past", () => {
		const now = 1_700_000_000_000;
		const previous = now - 1;
		expect(nextDailyAfter(previous, now)).toBe(previous + DAILY_INTERVAL_MS);
	});

	it("skips multiple missed days in a single step when far in the past", () => {
		const now = 1_700_000_000_000;
		const previous = now - 3 * DAILY_INTERVAL_MS - HOUR; // 3d 1h ago
		const next = nextDailyAfter(previous, now);
		expect(next).toBeGreaterThan(now);
		// Should be the next 24h-multiple boundary, not just +24h.
		expect((next - previous) % DAILY_INTERVAL_MS).toBe(0);
		expect(next - previous).toBe(4 * DAILY_INTERVAL_MS);
	});

	it("returns previous + 1 day exactly when previous == now", () => {
		const now = 1_700_000_000_000;
		expect(nextDailyAfter(now, now)).toBe(now + DAILY_INTERVAL_MS);
	});
});

describe("recoverScheduleEntry", () => {
	const now = 1_700_000_000_000;

	it("leaves a future entry unchanged and does not fire", () => {
		const stored = now + 5 * HOUR;
		expect(recoverScheduleEntry(stored, now)).toEqual({
			nextWakeAt: stored,
			shouldFireNow: false,
		});
	});

	it("fires now for an entry missed by less than the grace window", () => {
		const stored = now - HOUR; // 1h ago, within 2h grace
		const recovered = recoverScheduleEntry(stored, now);
		expect(recovered.shouldFireNow).toBe(true);
		expect(recovered.nextWakeAt).toBeGreaterThan(now);
	});

	it("fires now for an entry missed by exactly the grace window", () => {
		const stored = now - MISSED_WAKE_GRACE_MS;
		expect(recoverScheduleEntry(stored, now).shouldFireNow).toBe(true);
	});

	it("does NOT fire for an entry missed by more than the grace window", () => {
		const stored = now - MISSED_WAKE_GRACE_MS - 1;
		const recovered = recoverScheduleEntry(stored, now);
		expect(recovered.shouldFireNow).toBe(false);
		expect(recovered.nextWakeAt).toBeGreaterThan(now);
	});

	it("always returns a future nextWakeAt for past entries (regardless of grace)", () => {
		for (const ageDays of [0.1, 0.5, 1, 2, 7, 30]) {
			const stored = now - ageDays * DAILY_INTERVAL_MS;
			const { nextWakeAt } = recoverScheduleEntry(stored, now);
			expect(nextWakeAt, `age=${ageDays}d`).toBeGreaterThan(now);
		}
	});

	it("respects a custom grace window", () => {
		const stored = now - 10 * 60_000; // 10 min ago
		expect(recoverScheduleEntry(stored, now, 5 * 60_000).shouldFireNow).toBe(false);
		expect(recoverScheduleEntry(stored, now, 15 * 60_000).shouldFireNow).toBe(true);
	});
});

describe("resetHourFor / CLAUDE_RESET_OFFSET_HOURS", () => {
	it("adds the offset modulo 24", () => {
		expect(resetHourFor(0)).toBe(CLAUDE_RESET_OFFSET_HOURS);
		expect(resetHourFor(20)).toBe((20 + CLAUDE_RESET_OFFSET_HOURS) % 24);
		expect(resetHourFor(23)).toBe((23 + CLAUDE_RESET_OFFSET_HOURS) % 24);
	});

	it("wraps cleanly across midnight", () => {
		// Wake at 22:15, +5h = 03:00
		expect(resetHourFor(22)).toBe(3);
	});
});