/** * Acceptance proof for the two rules that cannot be eyeballed in production: * the double crons fire exactly once at 8 AM ET (digest) and midnight Saturday * ET (review issue) on BOTH UTC offsets, and NOTHING is ever scheduled on the * Sunday rest day — which is the whole reason the ladder's windows are 3 and 7. */ import { describe, expect, test } from "bun:test"; import { INTERVAL, RESERVED, SCHEDULE, STAGES, WINDOWS, addDays, campaignDay, campaignWeek, etDate, etHour, isRestDay, isoWeek, rng, sample, TEMPERATURES, temperatureOf, topicDate, weekdayOf, workingDay, } from "./srs.ts"; /** Every ET date of the campaign, plus the deferred-Hard tail. */ const CALENDAR = Array.from({ length: 70 }, (_, i) => addDays("2026-08-17", i)); describe("DST-proof cron guard", () => { test("digest fires only at ET hour 8 — EDT (UTC-4)", () => { // Cron pair 12:00 / 13:00 UTC during EDT (2026-08-29 is EDT). expect(etHour(new Date("2026-08-29T12:00:00Z"))).toBe(8); // fires expect(etHour(new Date("2026-08-29T13:00:00Z"))).toBe(9); // no-op }); test("digest fires only at ET hour 8 — EST (UTC-5)", () => { // DST ends 2026-11-01; 2026-11-05 is EST. expect(etHour(new Date("2026-11-05T12:00:00Z"))).toBe(7); // no-op expect(etHour(new Date("2026-11-05T13:00:00Z"))).toBe(8); // fires }); test("saturday review fires only at ET midnight — both offsets", () => { // EDT Saturday: 4 UTC = 0 ET fires, 5 UTC = 1 ET no-op. expect(etHour(new Date("2026-08-29T04:00:00Z"))).toBe(0); expect(weekdayOf(etDate(new Date("2026-08-29T04:00:00Z")))).toBe(6); expect(etHour(new Date("2026-08-29T05:00:00Z"))).toBe(1); // EST Saturday (2026-11-07): 5 UTC = 0 ET fires, 4 UTC = 11 PM FRIDAY ET. expect(etHour(new Date("2026-11-07T05:00:00Z"))).toBe(0); expect(weekdayOf(etDate(new Date("2026-11-07T05:00:00Z")))).toBe(6); expect(etHour(new Date("2026-11-07T04:00:00Z"))).toBe(23); expect(etDate(new Date("2026-11-07T04:00:00Z"))).toBe("2026-11-06"); // still Friday ET }); }); describe("date math (noon-UTC anchored)", () => { test("addDays crosses the DST-end boundary without drift", () => { expect(addDays("2026-10-31", 2)).toBe("2026-11-02"); expect(addDays("2026-08-24", 2)).toBe("2026-08-26"); }); test("campaign math", () => { expect(campaignDay("2026-08-17")).toBe(1); expect(campaignDay("2026-08-24")).toBe(8); expect(campaignWeek("2026-08-24")).toBe(2); expect(campaignWeek("2026-08-30")).toBe(2); expect(campaignWeek("2026-08-31")).toBe(3); }); test("iso week is stable across a week", () => { expect(isoWeek("2026-08-24")).toBe(isoWeek("2026-08-29")); expect(isoWeek("2026-08-30")).toBe(isoWeek("2026-08-24")); // Sun ends the ISO week expect(isoWeek("2026-08-31")).toBe(isoWeek("2026-08-24") + 1); }); }); describe("the rest day", () => { test("Sunday is the only rest day", () => { expect(isRestDay("2026-08-30")).toBe(true); // Sunday expect(isRestDay("2026-08-29")).toBe(false); // Saturday: gate day, still work expect(isRestDay("2026-08-31")).toBe(false); }); test("workingDay never returns a Sunday, from any day and any offset", () => { for (const date of CALENDAR) { for (let offset = 0; offset < 10; offset++) { expect(isRestDay(workingDay(date, offset))).toBe(false); } } }); test("workingDay(date) leaves a working day alone and slides Sunday to Monday", () => { expect(workingDay("2026-09-04")).toBe("2026-09-04"); // Friday expect(workingDay("2026-09-05")).toBe("2026-09-05"); // Saturday expect(workingDay("2026-09-06")).toBe("2026-09-07"); // Sunday → Monday }); test("offsets count working days, so a spill skips the rest day", () => { // Levelling from Saturday: tomorrow is Sunday, so the spill starts Monday. expect(workingDay("2026-09-05", 1)).toBe("2026-09-07"); expect(workingDay("2026-09-05", 2)).toBe("2026-09-08"); // Two deferred Hards per working day from a Monday. expect(workingDay("2026-09-28", 5)).toBe("2026-10-03"); // Saturday expect(workingDay("2026-09-28", 6)).toBe("2026-10-05"); // Sunday skipped }); test("every ladder interval lands on a working day, from every learning day", () => { for (const date of CALENDAR) { if (isRestDay(date)) continue; for (const days of WINDOWS) { const review = workingDay(addDays(date, days)); expect(isRestDay(review)).toBe(false); // Never earlier than the interval: a review may slip, never shorten. expect(review >= addDays(date, days)).toBe(true); } } }); test("only Thursday's +3 needs the slide", () => { const slid = CALENDAR.filter( (date) => !isRestDay(date) && workingDay(addDays(date, 3)) !== addDays(date, 3), ); expect(new Set(slid.map(weekdayOf))).toEqual(new Set([4])); // Thursday only for (const date of CALENDAR) { if (isRestDay(date)) continue; expect(workingDay(addDays(date, 7))).toBe(addDays(date, 7)); // +7 keeps the weekday } }); }); describe("the plan", () => { test("never books the rest day, and gates only on Saturday", () => { for (const [date, day] of Object.entries(SCHEDULE)) { expect(isRestDay(date)).toBe(false); if (day.gate) expect(weekdayOf(date)).toBe(6); if (day.topics) expect(weekdayOf(date)).toBeLessThan(6); } }); test("a topic is taught once; a doubled day answers for both", () => { const seen = new Set(); for (const day of Object.values(SCHEDULE)) { for (const topic of day.topics ?? []) { expect(seen.has(topic)).toBe(false); seen.add(topic); } } expect(topicDate(17)).toBe("2026-09-21"); expect(topicDate(18)).toBe("2026-09-21"); expect(topicDate(999)).toBeUndefined(); }); test("every gate and set problem is reserved from the samplers", () => { for (const day of Object.values(SCHEDULE)) { for (const lc of [...(day.gate ?? []), ...(day.problems ?? [])]) { expect(RESERVED.has(lc)).toBe(true); } } expect(RESERVED.size).toBeGreaterThan(0); }); }); describe("the ladder", () => { test("two rungs, named after the windows", () => { expect(STAGES).toEqual(["new", "+3", "+7", "retired"]); expect(WINDOWS.map((w) => INTERVAL[`+${w}`])).toEqual([...WINDOWS]); // Every non-terminal stage has an interval, and only those. expect(Object.keys(INTERVAL).sort()).toEqual(STAGES.slice(1, -1).sort()); }); }); describe("concept temperature", () => { test("the bands are the ladder's own windows", () => { // Boundaries are inclusive, and the bands tile the whole age axis with no // gap: whatever the windows become, every age still lands in exactly one. expect(TEMPERATURES).toEqual(["hot", "fresh", "fading", "cold"]); expect(temperatureOf(0)).toBe("hot"); expect(temperatureOf(WINDOWS[0])).toBe("hot"); expect(temperatureOf(WINDOWS[0] + 1)).toBe("fresh"); expect(temperatureOf(WINDOWS[1])).toBe("fresh"); expect(temperatureOf(WINDOWS[1] + 1)).toBe("fading"); expect(temperatureOf(WINDOWS[1] * 2)).toBe("fading"); expect(temperatureOf(WINDOWS[1] * 2 + 1)).toBe("cold"); }); test("never attempted is cold, not unknown", () => { expect(temperatureOf(null)).toBe("cold"); }); test("every age of the campaign has exactly one band", () => { for (let age = 0; age <= CALENDAR.length; age++) { expect(TEMPERATURES).toContain(temperatureOf(age)); } }); }); describe("deterministic sampling", () => { test("same seed, same picks", () => { const pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; expect(sample(pool, 3, rng("drill-2026-08-31"))).toEqual(sample(pool, 3, rng("drill-2026-08-31"))); expect(sample(pool, 3, rng("drill-2026-09-01"))).not.toEqual(sample(pool, 3, rng("drill-2026-08-31"))); }); });