179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
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<TOutput>(request: { prompt: string }): Promise<TOutput> {
|
|
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<TOutput>(): Promise<TOutput> {
|
|
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<ReturnType<typeof createDb>>) {
|
|
const structured: StructuredModelAdapter = {
|
|
provider: 'fake-structured',
|
|
model: 'schedule-model',
|
|
async generateObject<TOutput>(): Promise<TOutput> {
|
|
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,
|
|
});
|
|
}
|