T-HUB/packages/scheduling/src/materialize.test.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

111 lines
5.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { materializeOccurrence } from "./materialize-api.js";
import { compareWall } from "./nominal.js";
import { InvalidNominalLocalError } from "./errors.js";
import { wallOfInstant } from "./timezone.js";
const ISO = (ms: number) => new Date(ms).toISOString().replace(".000Z", "Z");
describe("materializeOccurrence — normal (unambiguous) times", () => {
it("resolves a plain New York evening slot", () => {
expect(ISO(materializeOccurrence("2024-03-09T21:00", "America/New_York")))
.toBe("2024-03-10T02:00:00Z");
});
it("passes UTC straight through", () => {
expect(ISO(materializeOccurrence("2024-03-10T02:30", "UTC")))
.toBe("2024-03-10T02:30:00Z");
});
it("resolves Kolkata (no DST, half-hour offset)", () => {
expect(ISO(materializeOccurrence("2024-06-01T09:00", "Asia/Kolkata")))
.toBe("2024-06-01T03:30:00Z");
});
});
describe("DST gap — nonexistent local time fires at the first valid instant after", () => {
it("2:30 on New York spring-forward (2024-03-10) shifts to 3:00 local / 07:00Z", () => {
const fire = materializeOccurrence("2024-03-10T02:30", "America/New_York");
expect(ISO(fire)).toBe("2024-03-10T07:00:00Z");
const wall = wallOfInstant(fire, "America/New_York");
expect(`${wall.hour}:${String(wall.minute).padStart(2, "0")}`).toBe("3:00");
});
it("2:45 shifts to 3:00 as well", () => {
expect(ISO(materializeOccurrence("2024-03-10T02:45", "America/New_York")))
.toBe("2024-03-10T07:00:00Z");
});
it("2:00 (whole gap hour) shifts to 3:00", () => {
expect(ISO(materializeOccurrence("2024-03-10T02:00", "America/New_York")))
.toBe("2024-03-10T07:00:00Z");
});
it("1:59 — last pre-gap minute — is untouched", () => {
expect(ISO(materializeOccurrence("2024-03-10T01:59", "America/New_York")))
.toBe("2024-03-10T06:59:00Z");
});
it("Berlin 02:30 spring-forward shifts to 03:00 local", () => {
// EU transitions at 01:00 UTC: 2024-03-31 02:00->03:00 local
expect(ISO(materializeOccurrence("2024-03-31T02:30", "Europe/Berlin")))
.toBe("2024-03-31T01:00:00Z");
});
it("whole-day gap: Pacific/Apia skipped 2011-12-30; a slot on that day fires at the first valid instant after the gap", () => {
// The zone jumped 2011-12-29T23:59:59-10:00 -> 2011-12-31T00:00:00+14:00,
// so 2011-12-30 never existed. First valid instant after the gap is
// 2011-12-31T00:00 local == 2011-12-30T10:00Z.
const fire = materializeOccurrence("2011-12-30T09:00", "Pacific/Apia");
expect(ISO(fire)).toBe("2011-12-30T10:00:00Z");
const wall = wallOfInstant(fire, "Pacific/Apia");
expect(`${wall.year}-${wall.month}-${String(wall.day).padStart(2, "0")}T${wall.hour}:${wall.minute}`).toBe("2011-12-31T0:0");
});
it("a slot just before the Apia skip (23:00 on 12-29) is untouched", () => {
expect(ISO(materializeOccurrence("2011-12-29T23:00", "Pacific/Apia")))
.toBe("2011-12-30T09:00:00Z");
});
});
describe("DST fold — ambiguous local time fires once, at the earlier instant", () => {
it("1:30 on New York fall-back (2024-11-03) fires at EDT (earlier offset)", () => {
// 01:30 EDT == 05:30Z; the later reading 01:30 EST == 06:30Z is NOT used.
const fire = materializeOccurrence("2024-11-03T01:30", "America/New_York");
expect(ISO(fire)).toBe("2024-11-03T05:30:00Z");
expect(fire).not.toBe(Date.parse("2024-11-03T06:30:00Z"));
});
it("1:30 fires exactly once at 05:30Z (not 06:30Z)", () => {
expect(ISO(materializeOccurrence("2024-11-03T01:30", "America/New_York")))
.toBe("2024-11-03T05:30:00Z");
});
it("1:00 fold hour fires at 05:00Z (earlier)", () => {
expect(ISO(materializeOccurrence("2024-11-03T01:00", "America/New_York")))
.toBe("2024-11-03T05:00:00Z");
});
it("2:00 after the fold is unambiguous again", () => {
expect(ISO(materializeOccurrence("2024-11-03T02:00", "America/New_York")))
.toBe("2024-11-03T07:00:00Z");
});
});
describe("validation", () => {
it("rejects malformed nominal strings", () => {
for (const bad of ["2024-3-10T02:30", "2024-03-10", "2024-03-10T2:30", "2024-13-01T00:00", "2024-03-32T00:00", "2024-03-10T25:00", "nonsense"]) {
expect(() => materializeOccurrence(bad, "UTC")).toThrow(InvalidNominalLocalError);
}
});
it("rejects invalid zones", () => {
expect(() => materializeOccurrence("2024-03-10T02:30", "America/Nowhere")).toThrow();
expect(() => materializeOccurrence("2024-03-10T02:30", "local")).toThrow();
});
it("is pure — repeated calls agree", () => {
const a = materializeOccurrence("2024-03-10T02:30", "America/New_York");
const b = materializeOccurrence("2024-03-10T02:30", "America/New_York");
const c = materializeOccurrence("2024-03-10T02:30", "America/New_York");
expect(a).toBe(b);
expect(b).toBe(c);
});
it("compareWall orders wall clocks chronologically", () => {
const w = (s: string) => ({
year: +s.slice(0, 4), month: +s.slice(5, 7), day: +s.slice(8, 10),
hour: +s.slice(11, 13), minute: +s.slice(14, 16), second: 0,
});
expect(compareWall(w("2024-03-10T01:59"), w("2024-03-10T03:00"))).toBeLessThan(0);
expect(compareWall(w("2024-11-03T01:30"), w("2024-11-03T01:30"))).toBe(0);
});
});
export {};