fix(02): correct DURATION-only event end in expand.ts (BUG 1)

- Replace dtend ?? dtstart with event.endDate which handles DURATION-only VEVENTs
- Add positive-duration guard (PT30M / P1D) to both non-recurring and recurring branches
- Add single-duration.ics fixture and regression test asserting end > start for DURATION-only events
This commit is contained in:
Lucas Berger
2026-06-05 14:33:00 -04:00
parent bef4a83fe0
commit ee2281fe81
3 changed files with 89 additions and 3 deletions
+25 -3
View File
@@ -207,9 +207,22 @@ export function expandOccurrences(
// --- 4. Non-recurring event: single occurrence check ---
if (!event.isRecurring()) {
if (dtstart.compare(rangeStart) >= 0 && dtstart.compare(rangeEnd) < 0) {
const dtend = (vevent.getFirstPropertyValue('dtend') as ICAL.Time | null) ?? dtstart
// Use ICAL.Event.endDate which derives end from DTEND, or DTSTART+DURATION, or sensible default.
// Do NOT use getFirstPropertyValue('dtend') directly — events with only DURATION set return null,
// producing zero-duration occurrences (BUG 1).
let occEnd: ICAL.Time = (event.endDate ?? dtstart) as ICAL.Time
// Positive-duration guard: ensure timed events have non-zero height in Schedule-X.
if (!allDay && occEnd.compare(dtstart) <= 0) {
occEnd = dtstart.clone()
occEnd.addDuration(ICAL.Duration.fromString('PT30M'))
} else if (allDay && occEnd.compare(dtstart) <= 0) {
occEnd = dtstart.clone()
occEnd.addDuration(ICAL.Duration.fromString('P1D'))
}
const start = serializeTime(dtstart, allDay)
const end = serializeTime(dtend, allDay)
const end = serializeTime(occEnd, allDay)
occurrences.push({
id: makeOccurrenceId(uid, dtstart),
uid,
@@ -240,9 +253,18 @@ export function expandOccurrences(
// Compute occurrence end from event duration
const duration = event.duration
const occEnd = next.clone()
let occEnd = next.clone()
occEnd.addDuration(duration)
// Positive-duration guard: same logic as non-recurring branch above.
if (!allDay && occEnd.compare(next) <= 0) {
occEnd = next.clone()
occEnd.addDuration(ICAL.Duration.fromString('PT30M'))
} else if (allDay && occEnd.compare(next) <= 0) {
occEnd = next.clone()
occEnd.addDuration(ICAL.Duration.fromString('P1D'))
}
const start = serializeTime(next, allDay)
const end = serializeTime(occEnd, allDay)