feat: add BoxBrain persona runtime APIs

This commit is contained in:
2026-05-11 17:01:19 +09:00
parent c5a3d7e835
commit 3ee6b233ea
13 changed files with 2043 additions and 9 deletions

178
tests/schedule.test.ts Normal file
View File

@@ -0,0 +1,178 @@
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,
});
}