/** * RED test scaffold: broker/vevent.ts — VEVENT builder (CAL-04, CAL-07) * * Behaviors under test: * 1. buildVeventString produces a VCALENDAR with a VEVENT for a timed event * with DTSTART using UTC 'Z' suffix (D-13 contract — no TZID) * 2. buildVeventString produces a VCALENDAR with DTSTART as a DATE value * (no time component, no TZID) for all-day events (D-13, Pitfall 3) * 3. buildVeventString with rruleString produces a VCALENDAR with an RRULE property (CAL-07) * * These tests FAIL (RED) because broker/vevent.ts does not exist yet. * They will turn GREEN in Plan 03-02 when the implementation is added. */ import { describe, it, expect } from 'vitest' // This import fails (RED) — broker/vevent.ts does not exist yet. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore intentional RED import import { buildVeventString } from '../../src/broker/vevent.js' describe('buildVeventString', () => { it('produces a VCALENDAR string containing a VEVENT for a timed event', () => { const result = buildVeventString({ summary: 'Team standup', allDay: false, dtstart: new Date('2026-06-10T09:00:00Z'), dtend: new Date('2026-06-10T09:30:00Z'), }) expect(result).toHaveProperty('uid') expect(result).toHaveProperty('icsString') expect(result.icsString).toContain('BEGIN:VCALENDAR') expect(result.icsString).toContain('BEGIN:VEVENT') expect(result.icsString).toContain('SUMMARY:Team standup') expect(result.icsString).toContain('END:VEVENT') expect(result.icsString).toContain('END:VCALENDAR') }) it('produces DTSTART with Z suffix (UTC) for a timed event — not TZID', () => { const result = buildVeventString({ summary: 'Morning meeting', allDay: false, dtstart: new Date('2026-06-10T14:00:00Z'), dtend: new Date('2026-06-10T15:00:00Z'), }) // D-13 timed contract: DATETIME in UTC → 'Z' suffix, no TZID expect(result.icsString).toMatch(/DTSTART:\d{8}T\d{6}Z/) expect(result.icsString).not.toMatch(/DTSTART;TZID=/) }) it('produces DTSTART as DATE (no time, no TZID) for an all-day event', () => { const result = buildVeventString({ summary: 'Birthday', allDay: true, dtstart: '2026-06-15', dtend: '2026-06-16', }) // D-13 all-day contract: VALUE=DATE, no time component, no TZID // ical.js represents DATE as DTSTART;VALUE=DATE:YYYYMMDD expect(result.icsString).toMatch(/DTSTART[^:]*:20260615/) // Must NOT contain a time component (no 'T' after the date) expect(result.icsString).not.toMatch(/DTSTART[^:]*:20260615T/) // Must NOT contain TZID on the DTSTART property expect(result.icsString).not.toMatch(/DTSTART;TZID=/) }) it('includes an RRULE property when rruleString is provided (CAL-07)', () => { const result = buildVeventString({ summary: 'Weekly sync', allDay: false, dtstart: new Date('2026-06-09T10:00:00Z'), dtend: new Date('2026-06-09T11:00:00Z'), rruleString: 'FREQ=WEEKLY;BYDAY=MO', }) expect(result.icsString).toContain('RRULE:FREQ=WEEKLY;BYDAY=MO') }) it('does NOT include RRULE when rruleString is omitted', () => { const result = buildVeventString({ summary: 'One-off lunch', allDay: false, dtstart: new Date('2026-06-10T12:00:00Z'), dtend: new Date('2026-06-10T13:00:00Z'), }) expect(result.icsString).not.toContain('RRULE:') }) it('uses the provided uid when given', () => { const uid = 'custom-uid-001@familysync' const result = buildVeventString({ uid, summary: 'Test event', allDay: false, dtstart: new Date('2026-06-10T09:00:00Z'), dtend: new Date('2026-06-10T10:00:00Z'), }) expect(result.uid).toBe(uid) expect(result.icsString).toContain(`UID:${uid}`) }) it('generates a uid when none is provided', () => { const result = buildVeventString({ summary: 'Auto uid event', allDay: false, dtstart: new Date('2026-06-10T09:00:00Z'), dtend: new Date('2026-06-10T10:00:00Z'), }) expect(result.uid).toBeTruthy() expect(result.uid.length).toBeGreaterThan(10) }) }) describe('buildVeventString — D-13 form-parsed contract', () => { it('timed event: produces BEGIN:VCALENDAR, SUMMARY, UID, timed DTSTART (Z suffix), and DTEND', () => { // Simulates the field shape parsed by the worker from the stored form JSON const result = buildVeventString({ uid: 'u1@familysync', summary: 'Lunch', allDay: false, dtstart: new Date('2026-06-10T12:00:00Z'), dtend: new Date('2026-06-10T13:00:00Z'), }) expect(result.icsString).toContain('BEGIN:VCALENDAR') expect(result.icsString).toContain('SUMMARY:Lunch') expect(result.icsString).toContain('UID:u1@familysync') // D-13 timed contract: DATETIME in UTC → 'Z' suffix, no TZID expect(result.icsString).toMatch(/DTSTART:\d{8}T\d{6}Z/) // DTEND must be present expect(result.icsString).toMatch(/DTEND:\d{8}T\d{6}Z/) }) it('single-day all-day event: DTSTART is DATE format and DTEND = DTSTART + 1 day (RFC-5545 exclusive end, WR-04)', () => { // Simulates a single-day all-day event where start and end are the same calendar day. // WR-04: the outbox worker passes the user-entered inclusive end; vevent.ts must advance it. const result = buildVeventString({ summary: 'Birthday', allDay: true, dtstart: '2026-06-10', dtend: '2026-06-10', }) // DTSTART must be DATE format (no time component, no TZID) — D-13 all-day contract expect(result.icsString).toMatch(/DTSTART[^:]*:20260610/) expect(result.icsString).not.toMatch(/DTSTART[^:]*:20260610T/) // WR-04: DTEND must be DTSTART + 1 day (RFC-5545 exclusive end) expect(result.icsString).toMatch(/DTEND[^:]*:20260611/) // DTEND date string must NOT equal DTSTART date string (owning-boundary assertion) const dtendMatch = result.icsString.match(/DTEND[^:]*:(\d{8})/) const dtstartMatch = result.icsString.match(/DTSTART[^:]*:(\d{8})/) expect(dtendMatch?.[1]).not.toBe(dtstartMatch?.[1]) }) })