test(06-03): add failing tests for hasRrule + bounded expansion
- Add hasRrule===true assertion for recurring events (weekly-dst.ics) - Add hasRrule===false assertion for non-recurring events (single-duration.ics) - Add weekly-count3.ics fixture (FREQ=WEEKLY;COUNT=3, 1-hour events) - Add bounded RRULE test: expects exactly 3 occurrences in wide window - Add per-occurrence duration test: each occurrence is 1 hour (not recurrence span) - Tests are RED: hasRrule field absent from CalendarOccurrence interface
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
* 3. EXDATE exclusions reduce the returned array by exactly one occurrence.
|
||||
* 4. Timed event start/end strings are IANA-annotated ('...±HH:MM[IANA/Zone]') so
|
||||
* Temporal.ZonedDateTime.from() can parse them without throwing (cross-contract test).
|
||||
* 5. hasRrule is true for recurring events, false for non-recurring (D-08).
|
||||
* 6. Bounded RRULE (COUNT=3) expands to exactly 3 occurrences; each occurrence's
|
||||
* duration derives from DTSTART→DTEND (not the recurrence span) (D-06 invariant).
|
||||
*/
|
||||
|
||||
import 'temporal-polyfill/global'
|
||||
@@ -182,6 +185,131 @@ describe('expandOccurrences', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasRrule field — D-08 (recurring series detection)', () => {
|
||||
it('recurring event occurrence has hasRrule === true', () => {
|
||||
// weekly-dst.ics has RRULE:FREQ=WEEKLY — all occurrences must have hasRrule === true
|
||||
const rawVevent = loadFixture('weekly-dst.ics')
|
||||
const windowStart = new Date('2026-03-01T00:00:00Z')
|
||||
const windowEnd = new Date('2026-04-01T00:00:00Z')
|
||||
|
||||
const occurrences = expandOccurrences(
|
||||
rawVevent,
|
||||
windowStart,
|
||||
windowEnd,
|
||||
1,
|
||||
'My Calendar',
|
||||
1,
|
||||
null,
|
||||
'#4A90D9',
|
||||
false,
|
||||
)
|
||||
|
||||
expect(occurrences.length).toBeGreaterThan(0)
|
||||
for (const occ of occurrences) {
|
||||
expect(occ.hasRrule).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('non-recurring event occurrence has hasRrule === false', () => {
|
||||
// single-duration.ics has no RRULE — the single occurrence must have hasRrule === false
|
||||
const rawVevent = loadFixture('single-duration.ics')
|
||||
const windowStart = new Date('2026-06-01T00:00:00Z')
|
||||
const windowEnd = new Date('2026-07-01T00:00:00Z')
|
||||
|
||||
const occurrences = expandOccurrences(
|
||||
rawVevent,
|
||||
windowStart,
|
||||
windowEnd,
|
||||
1,
|
||||
'My Calendar',
|
||||
1,
|
||||
null,
|
||||
'#4A90D9',
|
||||
false,
|
||||
)
|
||||
|
||||
expect(occurrences.length).toBe(1)
|
||||
expect(occurrences[0].hasRrule).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bounded RRULE (COUNT=3) — D-06 invariant', () => {
|
||||
it('expands to exactly 3 occurrences within a wide window (COUNT terminates expansion)', () => {
|
||||
// weekly-count3.ics has RRULE:FREQ=WEEKLY;COUNT=3 starting 2026-06-01.
|
||||
// A 6-month window must return exactly 3 occurrences — not more.
|
||||
const rawVevent = loadFixture('weekly-count3.ics')
|
||||
const windowStart = new Date('2026-06-01T00:00:00Z')
|
||||
const windowEnd = new Date('2026-12-01T00:00:00Z')
|
||||
|
||||
const occurrences = expandOccurrences(
|
||||
rawVevent,
|
||||
windowStart,
|
||||
windowEnd,
|
||||
1,
|
||||
'My Calendar',
|
||||
1,
|
||||
null,
|
||||
'#4A90D9',
|
||||
false,
|
||||
)
|
||||
|
||||
expect(occurrences.length).toBe(3)
|
||||
})
|
||||
|
||||
it('each bounded occurrence duration derives from DTSTART→DTEND (1 hour), not recurrence span', () => {
|
||||
// Each occurrence must have end exactly 1 hour after start.
|
||||
// The recurrence span is many weeks; duration must be per-event, not recurrence-level.
|
||||
const rawVevent = loadFixture('weekly-count3.ics')
|
||||
const windowStart = new Date('2026-06-01T00:00:00Z')
|
||||
const windowEnd = new Date('2026-12-01T00:00:00Z')
|
||||
|
||||
const occurrences = expandOccurrences(
|
||||
rawVevent,
|
||||
windowStart,
|
||||
windowEnd,
|
||||
1,
|
||||
'My Calendar',
|
||||
1,
|
||||
null,
|
||||
'#4A90D9',
|
||||
false,
|
||||
)
|
||||
|
||||
expect(occurrences.length).toBe(3)
|
||||
for (const occ of occurrences) {
|
||||
const startZdt = Temporal.ZonedDateTime.from(occ.start)
|
||||
const endZdt = Temporal.ZonedDateTime.from(occ.end)
|
||||
// Each occurrence must be exactly 1 hour (3 600 000 ms).
|
||||
// Use epochMilliseconds which is a regular number in the polyfill.
|
||||
const durationMs = endZdt.epochMilliseconds - startZdt.epochMilliseconds
|
||||
expect(durationMs).toBe(3_600_000)
|
||||
}
|
||||
})
|
||||
|
||||
it('bounded occurrences have hasRrule === true', () => {
|
||||
const rawVevent = loadFixture('weekly-count3.ics')
|
||||
const windowStart = new Date('2026-06-01T00:00:00Z')
|
||||
const windowEnd = new Date('2026-12-01T00:00:00Z')
|
||||
|
||||
const occurrences = expandOccurrences(
|
||||
rawVevent,
|
||||
windowStart,
|
||||
windowEnd,
|
||||
1,
|
||||
'My Calendar',
|
||||
1,
|
||||
null,
|
||||
'#4A90D9',
|
||||
false,
|
||||
)
|
||||
|
||||
expect(occurrences.length).toBe(3)
|
||||
for (const occ of occurrences) {
|
||||
expect(occ.hasRrule).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cross-contract: expand output → Temporal.ZonedDateTime.from (regression guard)', () => {
|
||||
it('timed event start/end strings from weekly-dst.ics parse via Temporal.ZonedDateTime.from without throwing', () => {
|
||||
// This is the integration test that was missing. It takes the actual serializeTime output
|
||||
|
||||
Reference in New Issue
Block a user