test(06-02): add failing tests for RRULE UNTIL/COUNT + FREQ persistence

- vevent.test.ts: add COUNT, UNTIL-DATE, UNTIL-DATETIME serialization assertions
- outboxWorker.test.ts: add assembleRruleString (D-06) describe block (not yet exported)
- outboxWorker.test.ts: add FREQ persistence (D-07 regression) describe block
- RED: assembleRruleString not yet exported; FREQ-persistence cases fail on missing helper
This commit is contained in:
Lucas Berger
2026-06-10 10:56:22 -04:00
parent 78a8cb5ccd
commit a59455a727
2 changed files with 104 additions and 1 deletions
+67 -1
View File
@@ -18,7 +18,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
// This import fails (RED) — broker/outboxWorker.ts does not exist yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore intentional RED import
import { runOutboxDrain } from '../../src/broker/outboxWorker.js'
import { runOutboxDrain, assembleRruleString } from '../../src/broker/outboxWorker.js'
// ── Drizzle DB mock ────────────────────────────────────────────────────────
// Follows the pattern from PATTERNS.md §Drizzle DB mock in tests.
@@ -647,3 +647,69 @@ describe('runOutboxDrain — fail closed on bad credentials (CR-03) + backoff in
expect(setArg?.nextAttemptAt?.getTime()).toBeLessThanOrEqual(expectedMaxMs)
})
})
// ─── D-06: assembleRruleString unit tests ─────────────────────────────────────
// These tests import the NOT-YET-EXPORTED `assembleRruleString` helper.
// RED: will fail because assembleRruleString is not exported yet.
describe('assembleRruleString (D-06)', () => {
it('returns base preset unchanged when no bound given', () => {
expect(assembleRruleString('FREQ=DAILY')).toBe('FREQ=DAILY')
})
it('appends COUNT when count is given (count wins over until)', () => {
expect(assembleRruleString('FREQ=DAILY', undefined, 5, false)).toBe('FREQ=DAILY;COUNT=5')
})
it('COUNT wins when both until and count are provided (mutual exclusion, RFC 5545 §3.3.10)', () => {
expect(assembleRruleString('FREQ=WEEKLY', '2026-06-30', 5, false)).toBe('FREQ=WEEKLY;COUNT=5')
})
it('appends UNTIL as DATE form (YYYYMMDD) for all-day events', () => {
expect(assembleRruleString('FREQ=WEEKLY', '2026-06-30', undefined, true)).toBe('FREQ=WEEKLY;UNTIL=20260630')
})
it('appends UNTIL as DATETIME UTC form (YYYYMMDDTHHMMSSZ) for timed events', () => {
expect(assembleRruleString('FREQ=WEEKLY', '2026-06-30', undefined, false)).toBe('FREQ=WEEKLY;UNTIL=20260630T235959Z')
})
it('COUNT=5 appended to FREQ=DAILY (matches plan behavior assertion)', () => {
expect(assembleRruleString('FREQ=DAILY', undefined, 5, false)).toBe('FREQ=DAILY;COUNT=5')
})
})
// ─── D-07: FREQ-persistence regression lock ────────────────────────────────────
// RED: will fail because the outbox worker does not yet wire recurrenceUntil/recurrenceCount
// and the FREQ-persistence assertion catches the D-07 regression scenario.
describe('FREQ persistence (D-07 regression)', () => {
beforeEach(() => {
vi.resetAllMocks()
mockPendingRows = []
wireMockChain()
})
it('D-07: daily-recurrence payload assembles to FREQ=DAILY (not weekly or none) in emitted ICS', async () => {
const { createCalendarEvent } = await import('../../src/broker/write.js')
let capturedIcsString: unknown = null
vi.mocked(createCalendarEvent).mockImplementation(async (_client, _cal, _uid, icsString) => {
capturedIcsString = icsString
return makeResponse(201)
})
const dailyPayload = JSON.stringify({
title: 'Daily standup',
allDay: false,
start: '2026-06-10T09:00:00',
end: '2026-06-10T09:30:00',
recurrence: 'daily',
})
mockPendingRows = [makeRow({ payload: dailyPayload })]
await runOutboxDrain()
expect(typeof capturedIcsString).toBe('string')
// D-07: FREQ must be DAILY — not WEEKLY or absent
expect(capturedIcsString as string).toContain('RRULE:FREQ=DAILY')
expect(capturedIcsString as string).not.toContain('FREQ=WEEKLY')
})
})
+37
View File
@@ -90,6 +90,43 @@ describe('buildVeventString', () => {
expect(result.icsString).not.toContain('RRULE:')
})
// D-06: RRULE COUNT — verified ical.js 2.2.1 output
it('serializes COUNT in RRULE for a timed event (D-06)', () => {
const result = buildVeventString({
summary: 'Weekly',
allDay: false,
dtstart: new Date('2026-06-10T09:00:00Z'),
dtend: new Date('2026-06-10T10:00:00Z'),
rruleString: 'FREQ=WEEKLY;COUNT=5',
})
expect(result.icsString).toContain('RRULE:FREQ=WEEKLY;COUNT=5')
})
// D-06: RRULE UNTIL DATE form — all-day event must NOT contain T235959Z
it('serializes UNTIL as DATE form for all-day events (D-06)', () => {
const result = buildVeventString({
summary: 'Daily standup',
allDay: true,
dtstart: '2026-06-10',
dtend: '2026-06-11',
rruleString: 'FREQ=DAILY;UNTIL=20260630',
})
expect(result.icsString).toContain('RRULE:FREQ=DAILY;UNTIL=20260630')
expect(result.icsString).not.toContain('T235959Z')
})
// D-06: RRULE UNTIL DATETIME UTC form — timed event
it('serializes UNTIL as DATETIME UTC form for timed events (D-06)', () => {
const result = buildVeventString({
summary: 'Weekly',
allDay: false,
dtstart: new Date('2026-06-10T09:00:00Z'),
dtend: new Date('2026-06-10T10:00:00Z'),
rruleString: 'FREQ=WEEKLY;UNTIL=20260630T235959Z',
})
expect(result.icsString).toContain('RRULE:FREQ=WEEKLY;UNTIL=20260630T235959Z')
})
it('uses the provided uid when given', () => {
const uid = 'custom-uid-001@familysync'
const result = buildVeventString({