calendar-ics-normalizer-unit.test.mjs
166 lines 5.0 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Tier 1 — UNIT: ICS normalizer and SourceCalendar defaults in isolation.
3 * Reference: docs/CALENDAR-EVENTS-V0-SPEC.md — Phase 1A
4 */
5 import { describe, it } from 'node:test';
6 import assert from 'node:assert/strict';
7 import {
8 parseIcsToEvents,
9 unfoldIcsLines,
10 parsePropertyLine,
11 unescapeIcsText,
12 normalizeStatus,
13 normalizeTimezoneId,
14 zonedLocalToUtc,
15 addDuration,
16 } from '../lib/calendar/ics-normalizer.mjs';
17 import {
18 SOURCE_CALENDAR_DEFAULTS,
19 buildSourceCalendarDefaults,
20 isAgentTierAllowed,
21 AGENT_CONTEXT_TIERS,
22 } from '../lib/calendar/source-calendar-defaults.mjs';
23 import { redactEventForAgentTier } from '../lib/calendar/agent-context-tier.mjs';
24
25 describe('SOURCE_CALENDAR_DEFAULTS', () => {
26 it('defaults display on, agents off, tier 0', () => {
27 assert.equal(SOURCE_CALENDAR_DEFAULTS.enabled_for_display, true);
28 assert.equal(SOURCE_CALENDAR_DEFAULTS.enabled_for_agents, false);
29 assert.equal(SOURCE_CALENDAR_DEFAULTS.agent_context_tier_max, 0);
30 assert.equal(SOURCE_CALENDAR_DEFAULTS.enabled_for_sync, true);
31 });
32
33 it('buildSourceCalendarDefaults rejects invalid tier', () => {
34 assert.throws(() => buildSourceCalendarDefaults({ agent_context_tier_max: 9 }), /0–4/);
35 });
36
37 it('isAgentTierAllowed is false for tier > 0 when agents disabled', () => {
38 const cal = buildSourceCalendarDefaults();
39 assert.equal(isAgentTierAllowed(cal, 0), true);
40 assert.equal(isAgentTierAllowed(cal, 1), false);
41 assert.equal(isAgentTierAllowed(cal, 2), false);
42 });
43 });
44
45 describe('unfoldIcsLines', () => {
46 it('joins folded continuation lines per RFC 5545 (removes CRLF + single whitespace)', () => {
47 const lines = unfoldIcsLines('SUMMARY:Hello wo\r\n rld\r\nUID:x');
48 assert.equal(lines[0], 'SUMMARY:Hello world');
49 assert.equal(lines[1], 'UID:x');
50 });
51 });
52
53 describe('parsePropertyLine', () => {
54 it('parses name, params, and value', () => {
55 const p = parsePropertyLine('DTSTART;TZID=America/Los_Angeles;VALUE=DATE:20260618');
56 assert.ok(p);
57 assert.equal(p.name, 'DTSTART');
58 assert.equal(p.params.TZID, 'America/Los_Angeles');
59 assert.equal(p.params.VALUE, 'DATE');
60 assert.equal(p.value, '20260618');
61 });
62 });
63
64 describe('unescapeIcsText', () => {
65 it('unescapes ICS text sequences', () => {
66 assert.equal(unescapeIcsText('a\\,b\\;c\\n d\\\\e'), 'a,b;c\n d\\e');
67 });
68 });
69
70 describe('normalizeStatus', () => {
71 it('maps ICS status strings to v0 enum', () => {
72 assert.equal(normalizeStatus('CANCELLED'), 'cancelled');
73 assert.equal(normalizeStatus('TENTATIVE'), 'tentative');
74 assert.equal(normalizeStatus(undefined), 'confirmed');
75 });
76 });
77
78 describe('normalizeTimezoneId', () => {
79 it('accepts valid IANA ids and rejects unknown', () => {
80 assert.equal(normalizeTimezoneId('UTC'), 'UTC');
81 assert.throws(() => normalizeTimezoneId('Not/A/Timezone'), /Unknown IANA/);
82 });
83 });
84
85 describe('zonedLocalToUtc', () => {
86 it('converts Pacific wall time to UTC (PDT in June)', () => {
87 const utc = zonedLocalToUtc(
88 { year: 2026, month: 6, day: 18, hour: 9, minute: 0, second: 0 },
89 'America/Los_Angeles',
90 );
91 assert.equal(utc.toISOString(), '2026-06-18T16:00:00.000Z');
92 });
93 });
94
95 describe('addDuration', () => {
96 it('adds ISO duration to start instant', () => {
97 const start = new Date('2026-06-23T10:00:00.000Z');
98 const end = addDuration(start, 'PT45M');
99 assert.equal(end.toISOString(), '2026-06-23T10:45:00.000Z');
100 });
101 });
102
103 describe('parseIcsToEvents — minimal inline ICS', () => {
104 const minimal = `BEGIN:VCALENDAR
105 BEGIN:VEVENT
106 UID:test@x
107 DTSTART:20260101T120000Z
108 DTEND:20260101T130000Z
109 SUMMARY:Hello
110 END:VEVENT
111 END:VCALENDAR`;
112
113 it('returns one normalized event', () => {
114 const events = parseIcsToEvents(minimal);
115 assert.equal(events.length, 1);
116 assert.equal(events[0].external_uid, 'test@x');
117 assert.equal(events[0].summary, 'Hello');
118 assert.equal(events[0].busy, true);
119 assert.equal(events[0].status, 'confirmed');
120 });
121
122 it('skips VEVENT without UID or DTSTART', () => {
123 const bad = `BEGIN:VCALENDAR
124 BEGIN:VEVENT
125 SUMMARY:No uid
126 END:VEVENT
127 END:VCALENDAR`;
128 assert.equal(parseIcsToEvents(bad).length, 0);
129 });
130 });
131
132 describe('redactEventForAgentTier', () => {
133 const event = {
134 external_uid: 'x@y',
135 start: '2026-06-18T17:00:00.000Z',
136 end: '2026-06-18T17:30:00.000Z',
137 timezone: 'UTC',
138 summary: 'Secret title',
139 busy: true,
140 status: 'confirmed',
141 recurrence_rule: null,
142 };
143
144 it('tier 0 returns null', () => {
145 assert.equal(redactEventForAgentTier(event, 0), null);
146 });
147
148 it('tier 1 omits summary', () => {
149 const r = redactEventForAgentTier(event, 1);
150 assert.ok(r);
151 assert.equal(r.summary, undefined);
152 });
153
154 it('tier 2 includes summary', () => {
155 const r = redactEventForAgentTier(event, 2, { calendar_label: 'Work' });
156 assert.ok(r);
157 assert.equal(r.summary, 'Secret title');
158 assert.equal(r.calendar_label, 'Work');
159 });
160 });
161
162 describe('AGENT_CONTEXT_TIERS', () => {
163 it('includes tiers 0 through 4', () => {
164 assert.deepEqual([...AGENT_CONTEXT_TIERS], [0, 1, 2, 3, 4]);
165 });
166 });
File History 1 commit
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago