From d18aba781631e0bc73934507604927e1f5974507 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 14 Jun 2026 08:15:06 -0400 Subject: [PATCH] =?UTF-8?q?test(11-05):=20RED=20=E2=80=94=20WR-01=20positi?= =?UTF-8?q?ve-duration=20TRIGGER=20classifies=20as=20custom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 new tests in classifyValarms suite asserting TRIGGER:+PT15M and TRIGGER:PT30M (positive/no-sign = fires after event) classify as {kind:'custom'}, not as preset/offlist. Negative triggers regression guards also present. 2 tests FAIL (RED): Math.abs() discards the sign, misclassifies as preset. --- apps/api/tests/broker/vevent.test.ts | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/api/tests/broker/vevent.test.ts b/apps/api/tests/broker/vevent.test.ts index e94dec7..529bb44 100644 --- a/apps/api/tests/broker/vevent.test.ts +++ b/apps/api/tests/broker/vevent.test.ts @@ -464,3 +464,43 @@ describe('computeAlertInstantUtc', () => { expect(result.toISOString()).toBe('2026-11-01T14:00:00.000Z'); }); }); + +// ─── Phase 11 Plan 05 WR-01: positive-duration TRIGGER classifies as custom ── + +describe('classifyValarms — WR-01: positive-duration trigger (fires after event)', () => { + // RFC 5545 allows TRIGGER:+PT15M — fires 15 min AFTER event start. + // Before the fix: Math.abs() discards the sign and classifies it as preset/offlist 15. + // After the fix: positive duration → {kind:'custom'} (preserve as-is). + + it('WR-01: TRIGGER:+PT15M classifies as custom, NOT preset', () => { + // TRIGGER:+PT15M is a post-event alarm; must not be mis-read as a 15-min-before lead. + // This test MUST FAIL before the fix. + const ics = makeIcs( + 'BEGIN:VALARM\r\nTRIGGER;RELATED=END:PT15M\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nEND:VALARM', + ); + expect(classifyValarms(ics)).toEqual({ kind: 'custom' }); + }); + + it('WR-01: TRIGGER:+PT30M classifies as custom, NOT offlist', () => { + // 30 min is in PRESET_MINUTES — without the sign check, Math.abs would give preset/30. + const ics = makeIcs( + 'BEGIN:VALARM\r\nTRIGGER:PT30M\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nEND:VALARM', + ); + expect(classifyValarms(ics)).toEqual({ kind: 'custom' }); + }); + + it('WR-01: negative TRIGGER:-PT15M still classifies as preset (unchanged behavior)', () => { + // Regression guard: negative triggers must not be affected by the fix. + const ics = makeIcs( + 'BEGIN:VALARM\r\nTRIGGER:-PT15M\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nEND:VALARM', + ); + expect(classifyValarms(ics)).toEqual({ kind: 'preset', leadMinutes: 15 }); + }); + + it('WR-01: negative off-list TRIGGER:-PT45M still classifies as offlist', () => { + const ics = makeIcs( + 'BEGIN:VALARM\r\nTRIGGER:-PT45M\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nEND:VALARM', + ); + expect(classifyValarms(ics)).toEqual({ kind: 'offlist', leadMinutes: 45 }); + }); +});