import { buildIcs } from './ics'; const NOW = new Date('2026-10-14T09:30:00.000Z'); describe('buildIcs', () => { it('writes all-day events with an exclusive end', () => { const ics = buildIcs( 'TaskStream', [ { uid: 'issue-1@taskstream', start: '2026-10-20', summary: 'WEB-1 · Login', }, { uid: 'sprint-1@taskstream', start: '2026-10-05', end: '2026-10-19', summary: 'Sprint 4', }, ], NOW, ); expect(ics.startsWith('BEGIN:VCALENDAR\r\nVERSION:2.0\r\n')).toBe(true); expect(ics.endsWith('END:VCALENDAR\r\n')).toBe(true); expect(ics).toContain('DTSTAMP:20261014T093000Z'); expect(ics).toContain( 'DTSTART;VALUE=DATE:20261020\r\nDTEND;VALUE=DATE:20261021', ); expect(ics).toContain( 'DTSTART;VALUE=DATE:20261005\r\nDTEND;VALUE=DATE:20261020', ); expect(ics.split('BEGIN:VEVENT')).toHaveLength(3); }); it('escapes text and skips what has no date', () => { const ics = buildIcs( 'Ana; López, QA', [ { uid: 'a', start: '2026-10-20', summary: 'Pago; roto, en\nSafari' }, { uid: 'b', start: '', summary: 'sin fecha' }, ], NOW, ); expect(ics).toContain('X-WR-CALNAME:Ana\\; López\\, QA'); expect(ics).toContain('SUMMARY:Pago\\; roto\\, en\\nSafari'); expect(ics).not.toContain('sin fecha'); }); it('folds long lines at 75 octets without splitting a character', () => { const summary = 'Revisión de la exportación á'.repeat(5); const ics = buildIcs( 'x', [{ uid: 'u', start: '2026-10-20', summary }], NOW, ); for (const line of ics.split('\r\n')) expect(Buffer.byteLength(line, 'utf8')).toBeLessThanOrEqual(75); // Unfolding gives the text back. expect(ics.replace(/\r\n /g, '')).toContain(`SUMMARY:${summary}`); }); });