import { afterEach, describe, expect, it } from 'vitest'; import { generateSchedule, getAvailabilitySnapshot, listScheduleEvents, pruneExpiredSchedule, pruneScheduleBefore, type ScheduleGenerationResult, type SpecialDateProvider, type StructuredModelAdapter, } from '../src'; import { closeOpenDbs, createDb } from './helpers'; afterEach(closeOpenDbs); describe('generateSchedule', () => { it('generates schedule events, stores them, and includes special date context in the prompt', async () => { const db = await createDb(); const prompts: string[] = []; const structured: StructuredModelAdapter = { provider: 'fake-structured', model: 'schedule-model', async generateObject(request: { prompt: string }): Promise { prompts.push(request.prompt); return { events: [ { title: 'Morning lecture', description: 'Regular major lecture on campus.', startAt: '2026-05-12T09:00:00.000Z', endAt: '2026-05-12T10:30:00.000Z', availabilityMode: 'do_not_disturb', availabilityReason: 'In class', kind: 'routine', topics: [{ name: 'lecture', category: 'concept' }], }, { title: 'Birthday dinner', description: 'Family dinner for a parent birthday.', startAt: '2026-05-12T18:00:00.000Z', endAt: '2026-05-12T20:00:00.000Z', availabilityMode: 'offline', availabilityReason: 'Family dinner', kind: 'special', topics: [{ name: 'birthday dinner', category: 'concept' }], }, ], } satisfies ScheduleGenerationResult as TOutput; }, }; const specialDateProvider: SpecialDateProvider = { async listSpecialDates() { return [ { date: '2026-05-12', title: 'Parents Day', description: 'A family-focused observance in Korea.', }, ]; }, }; const result = await generateSchedule(db, { spaceName: 'persona:minji', displayName: 'Minji', currentDate: '2026-05-12', scope: 'day', timezone: 'Asia/Seoul', structuredModel: structured, specialDateProvider, }); expect(prompts[0]).toContain('Parents Day'); expect(prompts[0]).toContain('Most of the time should remain routine'); expect(result.events.map((event) => event.title)).toEqual(['Morning lecture', 'Birthday dinner']); expect(result.availabilityEntries.map((entry) => entry.mode)).toEqual(['do_not_disturb', 'offline']); const stored = await listScheduleEvents(db, { spaceName: 'persona:minji' }); expect(stored.map((event) => event.title)).toEqual(['Morning lecture', 'Birthday dinner']); expect(stored[0]?.topics.map((topic) => topic.name)).toContain('2026-05-12'); }); it('can prune events that ended before a reference time', async () => { const db = await createDb(); await seedSchedule(db); const pruned = await pruneExpiredSchedule(db, { spaceName: 'persona:minji', referenceTime: '2026-05-12T12:00:00.000Z', graceSeconds: 0, }); expect(pruned.deletedEventIds).toHaveLength(1); expect((await listScheduleEvents(db, { spaceName: 'persona:minji' })).map((event) => event.title)).toEqual(['Evening shift']); const snapshot = await getAvailabilitySnapshot(db, { spaceName: 'persona:minji', at: '2026-05-12T09:30:00.000Z', }); expect(snapshot.current.mode).toBe('online'); expect(snapshot.current.sourceType).toBe('default'); }); it('can prune events scheduled before a cutoff date', async () => { const db = await createDb(); await seedSchedule(db); const pruned = await pruneScheduleBefore(db, { spaceName: 'persona:minji', before: '2026-05-12T18:00:00.000Z', }); expect(pruned.deletedEventIds).toHaveLength(1); expect((await listScheduleEvents(db, { spaceName: 'persona:minji' })).map((event) => event.title)).toEqual(['Evening shift']); }); it('rejects invalid schedule timestamps from the structured model', async () => { const db = await createDb(); const structured: StructuredModelAdapter = { provider: 'fake-structured', model: 'schedule-model', async generateObject(): Promise { return { events: [ { title: 'Broken event', startAt: 'not-a-date', endAt: '2026-05-12T10:00:00.000Z', availabilityMode: 'online', }, ], } satisfies ScheduleGenerationResult as TOutput; }, }; await expect(generateSchedule(db, { spaceName: 'persona:minji', displayName: 'Minji', currentDate: '2026-05-12', scope: 'day', structuredModel: structured, })).rejects.toThrow('Schedule event at index 0 startAt must be a valid ISO timestamp.'); }); }); async function seedSchedule(db: Awaited>) { const structured: StructuredModelAdapter = { provider: 'fake-structured', model: 'schedule-model', async generateObject(): Promise { return { events: [ { title: 'Morning lecture', startAt: '2026-05-12T09:00:00.000Z', endAt: '2026-05-12T10:30:00.000Z', availabilityMode: 'do_not_disturb', kind: 'routine', }, { title: 'Evening shift', startAt: '2026-05-12T18:00:00.000Z', endAt: '2026-05-12T22:00:00.000Z', availabilityMode: 'offline', kind: 'routine', }, ], } satisfies ScheduleGenerationResult as TOutput; }, }; await generateSchedule(db, { spaceName: 'persona:minji', displayName: 'Minji', currentDate: '2026-05-12', scope: 'day', structuredModel: structured, }); }