Milestone v1.0: FamilySync MVP #1
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user