summaryrefslogtreecommitdiffhomepage
path: root/packages/api/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/api/tests')
-rw-r--r--packages/api/tests/wake-scheduler.test.ts98
1 files changed, 98 insertions, 0 deletions
diff --git a/packages/api/tests/wake-scheduler.test.ts b/packages/api/tests/wake-scheduler.test.ts
new file mode 100644
index 0000000..0e5731c
--- /dev/null
+++ b/packages/api/tests/wake-scheduler.test.ts
@@ -0,0 +1,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);
+ });
+});