import { describe, expect, it } from "vitest"; import { addBusinessDays, compareArithmetic } from "../../src/time/arithmetic"; import { inspectEpoch } from "../../src/time/epoch"; import { createUtcEvent, foldIcsLine } from "../../src/time/ics"; import { inspectIcs } from "../../src/time/ics-inspect"; import { planMeetings } from "../../src/time/meeting"; import { previewCron, previewCronDialect, previewRRule, } from "../../src/time/recurrence"; import { compareTimeZones, nextTransitions, resolveLocalDateTime, } from "../../src/time/zones"; describe("exact epoch conversion", () => { it("preserves negative nanoseconds", () => { const result = inspectEpoch("-0.000000001", "seconds"); expect(result.epochNanoseconds).toBe(-1n); expect(result.iso).toBe("1969-12-31T23:59:59.999999999Z"); }); it("rejects sub-nanosecond precision", () => { expect(() => inspectEpoch("0.0000000001", "seconds")).toThrow( /smaller than one nanosecond/u, ); }); }); describe("time-zone handling", () => { it("classifies a spring gap and autumn overlap", () => { expect( resolveLocalDateTime("2026-03-29T02:30", "Europe/Berlin").status, ).toBe("skipped"); const overlap = resolveLocalDateTime("2026-10-25T02:30", "Europe/Berlin"); expect(overlap.status).toBe("ambiguous"); expect(overlap.choices.map((choice) => choice.offset)).toEqual([ "+02:00", "+01:00", ]); }); it("compares the same instant without changing its epoch", () => { const values = compareTimeZones("2026-01-01T00:00:00Z", [ "UTC", "Asia/Tokyo", ]); expect(values[0]?.epochNanoseconds).toBe(values[1]?.epochNanoseconds); expect(values[1]?.local).toBe("2026-01-01T09:00:00"); }); it("rejects a non-integer transition count", () => { expect(() => nextTransitions("2026-01-01T00:00:00Z", "Europe/Berlin", 1.5), ).toThrow(/between 1 and 32/u); }); }); describe("calendar arithmetic", () => { it("shows wall-clock and elapsed days diverging across DST", () => { const result = compareArithmetic( "2026-03-28T12:00:00Z", "Europe/Berlin", "P1D", ); expect(result.differenceSeconds).toBe("-3600"); }); it("adds business days with explicit holidays", () => { expect(addBusinessDays("2026-12-24", 2, ["2026-12-25"]).result).toBe( "2026-12-29", ); }); it("bounds explicit holiday allocation", () => { expect(() => addBusinessDays("2026-01-01", 1, Array(10_001).fill("2026-01-02")), ).toThrow(/10,000/u); }); it("does not silently truncate a fractional business-day count", () => { expect(() => addBusinessDays("2026-01-01", 1.5)).toThrow(/±100,000/u); }); }); describe("bounded recurrence previews", () => { it("previews five-field cron in its named zone", () => { const result = previewCron( "0 9 * * MON-FRI", "2026-08-31T08:00:00Z", "Europe/Berlin", 2, "5-part", ); expect(result).toHaveLength(2); expect(result[0]?.zoned).toContain("T09:00:00"); }); it("previews a strict RRULE", () => { const result = previewRRule( "FREQ=DAILY;COUNT=3", "2026-09-01T09:00", "Europe/Berlin", 10, ); expect(result).toHaveLength(3); expect(result[0]?.zoned).toContain("2026-09-01T09:00:00"); }); it("makes cron dialect field and timezone semantics explicit", () => { const unix = previewCronDialect( "0 9 * * MON-FRI", "2026-09-01T00:00:00Z", "Europe/Berlin", 2, "unix-vixie", ); expect(unix).toMatchObject({ dialect: "unix-vixie", dayCombination: "or", }); expect(unix.occurrences[0]?.zoned).toContain("T09:00:00"); expect(() => previewCronDialect( "0 9 * * *", "2026-09-01T00:00:00Z", "Europe/Berlin", 1, "github-actions", ), ).toThrow(/UTC only/u); expect( previewCronDialect( "* * * * *", "2026-09-01T00:00:00Z", "UTC", 2, "github-actions", ).warnings.join(" "), ).toMatch(/five-minute minimum/u); const aws = previewCronDialect( "0 9 ? * MON-FRI 2026", "2026-09-01T00:00:00Z", "UTC", 1, "aws-eventbridge", ); expect(aws.normalizedPattern).toBe("0 0 9 ? * MON-FRI 2026"); expect(aws.dayCombination).toBe("exclusive-question-mark"); }); }); describe("time-zone meeting planning", () => { it("finds only shared recurring local work-hour windows", () => { const result = planMeetings({ start: "2026-09-01T00:00:00Z", days: 2, durationMinutes: 60, stepMinutes: 30, maxResults: 2, participants: [ { name: "Berlin", timeZone: "Europe/Berlin", workStart: "09:00", workEnd: "17:00", }, { name: "New York", timeZone: "America/New_York", workStart: "09:00", workEnd: "17:00", }, ], }); expect(result.candidates).toHaveLength(2); expect( result.candidates[0]?.participants.map((item) => item.startLocal), ).toEqual([ expect.stringContaining("T15:00:00"), expect.stringContaining("T09:00:00"), ]); }); it("bounds participants and rejects overnight work-window ambiguity", () => { expect(() => planMeetings({ start: "2026-09-01T00:00:00Z", days: 1, durationMinutes: 60, stepMinutes: 30, participants: [ { name: "Night", timeZone: "UTC", workStart: "22:00", workEnd: "06:00", }, ], }), ).toThrow(/end after/u); }); }); describe("iCalendar output", () => { it("creates CRLF UTC event lines", () => { const result = createUtcEvent({ startLocal: "2026-09-01T10:00", timeZone: "Europe/Berlin", duration: "PT1H", summary: "Review, plan", }); expect(result.ics).toContain("DTSTART:20260901T080000Z\r\n"); expect(result.ics).toContain("SUMMARY:Review\\, plan"); expect(result.ics.endsWith("\r\n")).toBe(true); }); it("folds Unicode lines by UTF-8 octets", () => { const folded = foldIcsLine(`DESCRIPTION:${"ä".repeat(50)}`); expect(folded).toContain("\r\n "); expect( Math.max( ...folded .split("\r\n") .map((line) => new TextEncoder().encode(line).length), ), ).toBeLessThanOrEqual(75); }); }); describe("iCalendar import and inspection", () => { it("imports multiple events and applies exclusions and moved/cancelled overrides", () => { const result = inspectIcs( [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Fixture//EN", "BEGIN:VEVENT", "UID:series@example", "DTSTART;TZID=Europe/Berlin:20260901T090000", "RRULE:FREQ=DAILY;COUNT=4", "EXDATE;TZID=Europe/Berlin:20260902T090000", "SUMMARY:Master", "END:VEVENT", "BEGIN:VEVENT", "UID:series@example", "RECURRENCE-ID;TZID=Europe/Berlin:20260903T090000", "DTSTART;TZID=Europe/Berlin:20260903T110000", "SUMMARY:Moved", "END:VEVENT", "BEGIN:VEVENT", "UID:series@example", "RECURRENCE-ID;TZID=Europe/Berlin:20260904T090000", "DTSTART;TZID=Europe/Berlin:20260904T090000", "STATUS:CANCELLED", "SUMMARY:Cancelled", "END:VEVENT", "BEGIN:VEVENT", "UID:single@example", "DTSTART:20260905T120000Z", "SUMMARY:Single", "URL:https://example.invalid/private", "ATTACH;FMTTYPE=application/pdf:https://example.invalid/file.pdf", "END:VEVENT", "END:VCALENDAR", "", ].join("\r\n"), 20, ); expect(result.events).toHaveLength(4); expect(result.occurrences.map((item) => item.summary)).toEqual([ "Master", "Moved", "Single", ]); expect(result.occurrences[1]).toMatchObject({ source: "override" }); expect(result.references).toHaveLength(2); expect(result.references.every((reference) => reference.inert)).toBe(true); }); it("inspects VTIMEZONE while withholding unknown custom-zone expansion", () => { const result = inspectIcs( [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Fixture//EN", "BEGIN:VTIMEZONE", "TZID:Custom/Office", "BEGIN:STANDARD", "DTSTART:19700101T000000", "TZOFFSETFROM:+0100", "TZOFFSETTO:+0100", "END:STANDARD", "END:VTIMEZONE", "BEGIN:VEVENT", "UID:custom@example", "DTSTART;TZID=Custom/Office:20260901T090000", "RRULE:FREQ=DAILY;COUNT=2", "SUMMARY:Custom zone", "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png:AAAA", "END:VEVENT", "END:VCALENDAR", ].join("\r\n"), ); expect(result.timeZones[0]).toMatchObject({ tzid: "Custom/Office", usableByBrowser: false, }); expect(result.references[0]).toMatchObject({ kind: "embedded-binary", encodedCharacters: 4, }); expect(result.diagnostics.map((item) => item.code)).toContain( "custom-vtimezone", ); expect(result.occurrences).toEqual([]); }); it("expands all-day recurrences without pretending they are instants", () => { const result = inspectIcs( [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Fixture//EN", "BEGIN:VEVENT", "UID:days@example", "DTSTART;VALUE=DATE:20260901", "RRULE:FREQ=DAILY;COUNT=3", "EXDATE;VALUE=DATE:20260902", "SUMMARY:All day", "END:VEVENT", "END:VCALENDAR", ].join("\r\n"), ); expect(result.occurrences.map((item) => item.start)).toEqual([ "2026-09-01 (all-day)", "2026-09-03 (all-day)", ]); }); it("rejects mismatched and excessively nested components", () => { expect(() => inspectIcs("BEGIN:VCALENDAR\r\nEND:VEVENT\r\n")).toThrow( /Mismatched/u, ); const nested = [ "BEGIN:VCALENDAR", ...Array(17).fill("BEGIN:X"), ...Array(17).fill("END:X"), "END:VCALENDAR", ].join("\r\n"); expect(() => inspectIcs(nested)).toThrow(/nesting limit/u); }); });