feat: add BoxBrain persona runtime APIs
This commit is contained in:
143
tests/availability.test.ts
Normal file
143
tests/availability.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
generateSchedule,
|
||||
getAvailabilitySnapshot,
|
||||
setAvailabilityStatus,
|
||||
type ScheduleGenerationResult,
|
||||
type StructuredModelAdapter,
|
||||
} from '../src';
|
||||
import { closeOpenDbs, createDb } from './helpers';
|
||||
|
||||
afterEach(closeOpenDbs);
|
||||
|
||||
describe('availability status APIs', () => {
|
||||
it('defaults to online when no explicit status exists', async () => {
|
||||
const db = await createDb();
|
||||
|
||||
const snapshot = await getAvailabilitySnapshot(db, {
|
||||
spaceName: 'persona:minji',
|
||||
at: '2026-05-12T08:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(snapshot.current.mode).toBe('online');
|
||||
expect(snapshot.current.sourceType).toBe('default');
|
||||
});
|
||||
|
||||
it('derives current and next availability from the stored schedule', async () => {
|
||||
const db = await createDb();
|
||||
await seedSchedule(db);
|
||||
|
||||
const classTime = await getAvailabilitySnapshot(db, {
|
||||
spaceName: 'persona:minji',
|
||||
at: '2026-05-12T09:15:00.000Z',
|
||||
});
|
||||
expect(classTime.current.mode).toBe('do_not_disturb');
|
||||
expect(classTime.current.reason).toBe('In class');
|
||||
expect(classTime.next?.mode).toBe('online');
|
||||
expect(classTime.next?.effectiveFrom).toBe('2026-05-12T10:30:00.000Z');
|
||||
|
||||
const afterDinner = await getAvailabilitySnapshot(db, {
|
||||
spaceName: 'persona:minji',
|
||||
at: '2026-05-12T21:30:00.000Z',
|
||||
});
|
||||
expect(afterDinner.current.mode).toBe('offline');
|
||||
expect(afterDinner.current.until).toBe('2026-05-12T22:00:00.000Z');
|
||||
});
|
||||
|
||||
it('allows explicit status updates that override schedule-derived status', async () => {
|
||||
const db = await createDb();
|
||||
await seedSchedule(db);
|
||||
|
||||
await setAvailabilityStatus(db, {
|
||||
spaceName: 'persona:minji',
|
||||
mode: 'online',
|
||||
reason: 'Taking a quick break before class starts again.',
|
||||
effectiveFrom: '2026-05-12T09:10:00.000Z',
|
||||
until: '2026-05-12T09:40:00.000Z',
|
||||
sourceType: 'tool',
|
||||
});
|
||||
|
||||
const snapshot = await getAvailabilitySnapshot(db, {
|
||||
spaceName: 'persona:minji',
|
||||
at: '2026-05-12T09:20:00.000Z',
|
||||
});
|
||||
|
||||
expect(snapshot.current.mode).toBe('online');
|
||||
expect(snapshot.current.sourceType).toBe('tool');
|
||||
expect(snapshot.current.reason).toContain('quick break');
|
||||
expect(snapshot.next?.mode).toBe('do_not_disturb');
|
||||
expect(snapshot.next?.effectiveFrom).toBe('2026-05-12T09:40:00.000Z');
|
||||
});
|
||||
|
||||
it('keeps caller metadata separate from reserved availability fields', async () => {
|
||||
const db = await createDb();
|
||||
|
||||
const entry = await setAvailabilityStatus(db, {
|
||||
spaceName: 'persona:minji',
|
||||
mode: 'online',
|
||||
effectiveFrom: '2026-05-12T08:00:00.000Z',
|
||||
metadata: {
|
||||
id: 'caller-id',
|
||||
mode: 'offline',
|
||||
note: 'still just metadata',
|
||||
},
|
||||
});
|
||||
|
||||
expect(entry.id).not.toBe('caller-id');
|
||||
expect(entry.mode).toBe('online');
|
||||
expect(entry.metadata).toEqual({
|
||||
id: 'caller-id',
|
||||
mode: 'offline',
|
||||
note: 'still just metadata',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid availability timestamps', async () => {
|
||||
const db = await createDb();
|
||||
|
||||
await expect(setAvailabilityStatus(db, {
|
||||
spaceName: 'persona:minji',
|
||||
mode: 'online',
|
||||
effectiveFrom: 'not-a-date',
|
||||
})).rejects.toThrow('Availability effectiveFrom 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',
|
||||
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',
|
||||
},
|
||||
{
|
||||
title: 'Family dinner',
|
||||
description: 'Dinner with family.',
|
||||
startAt: '2026-05-12T18:00:00.000Z',
|
||||
endAt: '2026-05-12T22:00:00.000Z',
|
||||
availabilityMode: 'offline',
|
||||
availabilityReason: 'Family dinner',
|
||||
kind: 'special',
|
||||
},
|
||||
],
|
||||
} satisfies ScheduleGenerationResult as TOutput;
|
||||
},
|
||||
};
|
||||
|
||||
await generateSchedule(db, {
|
||||
spaceName: 'persona:minji',
|
||||
displayName: 'Minji',
|
||||
currentDate: '2026-05-12',
|
||||
scope: 'day',
|
||||
structuredModel: structured,
|
||||
});
|
||||
}
|
||||
258
tests/conversation.test.ts
Normal file
258
tests/conversation.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
generateSchedule,
|
||||
getAvailabilitySnapshot,
|
||||
listConversationEntries,
|
||||
replyToConversation,
|
||||
setAvailabilityStatus,
|
||||
startConversation,
|
||||
type ConversationMemorySelectionResult,
|
||||
type ConversationTurnPlan,
|
||||
type ScheduleGenerationResult,
|
||||
type StructuredModelAdapter,
|
||||
} from '../src';
|
||||
import { persistFactDrafts } from '../src/memory';
|
||||
import { closeOpenDbs, createDb } from './helpers';
|
||||
|
||||
afterEach(closeOpenDbs);
|
||||
|
||||
describe('conversation APIs', () => {
|
||||
it('blocks replies while the persona is offline', async () => {
|
||||
const db = await createDb();
|
||||
await db.upsertSpace({
|
||||
name: 'persona:minji',
|
||||
metadata: { boxbrain: { domain: 'persona.space', personaId: 'minji', displayName: 'Minji' } },
|
||||
});
|
||||
await setAvailabilityStatus(db, {
|
||||
spaceName: 'persona:minji',
|
||||
mode: 'offline',
|
||||
reason: 'Sleeping',
|
||||
effectiveFrom: '2026-05-12T00:00:00.000Z',
|
||||
until: '2026-05-12T08:00:00.000Z',
|
||||
sourceType: 'manual',
|
||||
});
|
||||
|
||||
let responseCalls = 0;
|
||||
const result = await replyToConversation(db, {
|
||||
spaceName: 'persona:minji',
|
||||
counterpartId: 'user:shinwoo',
|
||||
counterpartDisplayName: 'Shinwoo',
|
||||
message: '지금뭐해',
|
||||
currentTime: '2026-05-12T07:00:00.000Z',
|
||||
mandatoryMemoryModel: createSelectionModel([]),
|
||||
contextualMemoryModel: createSelectionModel([]),
|
||||
responseModel: {
|
||||
provider: 'fake-structured',
|
||||
model: 'response-model',
|
||||
async generateObject<TOutput>(): Promise<TOutput> {
|
||||
responseCalls += 1;
|
||||
return { mode: 'reply', messages: ['안자'] } as TOutput;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.messages).toEqual([]);
|
||||
expect(responseCalls).toBe(0);
|
||||
});
|
||||
|
||||
it('retrieves mandatory/contextual memories, generates DM-style replies, and stores the turn', async () => {
|
||||
const db = await createDb();
|
||||
await seedPersonaMemory(db);
|
||||
const mandatoryPrompts: string[] = [];
|
||||
const contextualPrompts: string[] = [];
|
||||
const responsePrompts: string[] = [];
|
||||
|
||||
const result = await replyToConversation(db, {
|
||||
spaceName: 'persona:minji',
|
||||
counterpartId: 'user:shinwoo',
|
||||
counterpartDisplayName: 'Shinwoo',
|
||||
message: '오늘저녁뭐해?',
|
||||
currentTime: '2026-05-12T12:00:00.000Z',
|
||||
mandatoryMemoryModel: createSelectionModel(['m1', 'm2'], mandatoryPrompts),
|
||||
contextualMemoryModel: createSelectionModel(['m3'], contextualPrompts),
|
||||
responseModel: createResponseModel(
|
||||
{
|
||||
mode: 'reply',
|
||||
messages: ['저녁엔가족이랑먹어', '왜궁금해'],
|
||||
},
|
||||
responsePrompts,
|
||||
),
|
||||
rng: () => 0,
|
||||
});
|
||||
|
||||
expect(mandatoryPrompts[0]).toContain('yesterday, today, and tomorrow schedules');
|
||||
expect(contextualPrompts[0]).toContain('오늘저녁뭐해?');
|
||||
expect(responsePrompts[0]).toContain('family dinner');
|
||||
expect(result.messages).toHaveLength(2);
|
||||
expect(result.messages[0]?.replyDelaySeconds).toBe(1);
|
||||
expect(result.messages[0]?.typingDelaySeconds).toBeGreaterThan(0);
|
||||
expect(result.messages[1]?.replyDelaySeconds).toBe(0);
|
||||
expect(result.usedMemories).toHaveLength(3);
|
||||
|
||||
const history = await listConversationEntries(db, { spaceName: 'persona:minji', counterpartId: 'user:shinwoo' });
|
||||
expect(history.map((entry) => `${entry.direction}:${entry.text}`)).toEqual([
|
||||
'inbound:오늘저녁뭐해?',
|
||||
'outbound:저녁엔가족이랑먹어',
|
||||
'outbound:왜궁금해',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can proactively start a conversation without inbound user text', async () => {
|
||||
const db = await createDb();
|
||||
await seedPersonaMemory(db);
|
||||
|
||||
const result = await startConversation(db, {
|
||||
spaceName: 'persona:minji',
|
||||
counterpartId: 'user:shinwoo',
|
||||
counterpartDisplayName: 'Shinwoo',
|
||||
currentTime: '2026-05-12T15:00:00.000Z',
|
||||
mandatoryMemoryModel: createSelectionModel(['m1']),
|
||||
contextualMemoryModel: createSelectionModel([]),
|
||||
responseModel: createResponseModel({ mode: 'reply', messages: ['지금뭐해'] }),
|
||||
rng: () => 0,
|
||||
});
|
||||
|
||||
expect(result.messages.map((message) => message.text)).toEqual(['지금뭐해']);
|
||||
const history = await listConversationEntries(db, { spaceName: 'persona:minji', counterpartId: 'user:shinwoo' });
|
||||
expect(history[0]?.proactive).toBe(true);
|
||||
});
|
||||
|
||||
it('executes availability tool calls after farewell-style refusal messages', async () => {
|
||||
const db = await createDb();
|
||||
await seedPersonaMemory(db);
|
||||
await generateSchedule(db, {
|
||||
spaceName: 'persona:minji',
|
||||
displayName: 'Minji',
|
||||
currentDate: '2026-05-12',
|
||||
scope: 'day',
|
||||
structuredModel: {
|
||||
provider: 'fake-structured',
|
||||
model: 'schedule-model',
|
||||
async generateObject<TOutput>(): Promise<TOutput> {
|
||||
return {
|
||||
events: [
|
||||
{
|
||||
title: 'Exam',
|
||||
startAt: '2026-05-12T10:00:00.000Z',
|
||||
endAt: '2026-05-12T12:00:00.000Z',
|
||||
availabilityMode: 'offline',
|
||||
availabilityReason: 'Exam starting',
|
||||
kind: 'special',
|
||||
},
|
||||
],
|
||||
} satisfies ScheduleGenerationResult as TOutput;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const prompts: string[] = [];
|
||||
const result = await startConversation(db, {
|
||||
spaceName: 'persona:minji',
|
||||
counterpartId: 'user:shinwoo',
|
||||
counterpartDisplayName: 'Shinwoo',
|
||||
currentTime: '2026-05-12T09:55:00.000Z',
|
||||
mandatoryMemoryModel: createSelectionModel(['m1']),
|
||||
contextualMemoryModel: createSelectionModel([]),
|
||||
responseModel: createResponseModel(
|
||||
{
|
||||
mode: 'refuse',
|
||||
messages: ['나이제가봐야해', '이따연락해'],
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'setAvailabilityStatus',
|
||||
arguments: {
|
||||
mode: 'offline',
|
||||
reason: 'Exam starting',
|
||||
effectiveFrom: '2026-05-12T09:58:00.000Z',
|
||||
until: '2026-05-12T12:00:00.000Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
prompts,
|
||||
),
|
||||
});
|
||||
|
||||
expect(prompts[0]).toContain('next availability transition');
|
||||
expect(result.messages.map((message) => message.text)).toEqual(['나이제가봐야해', '이따연락해']);
|
||||
|
||||
const snapshot = await getAvailabilitySnapshot(db, {
|
||||
spaceName: 'persona:minji',
|
||||
at: '2026-05-12T10:00:00.000Z',
|
||||
});
|
||||
expect(snapshot.current.mode).toBe('offline');
|
||||
expect(snapshot.current.reason).toBe('Exam starting');
|
||||
});
|
||||
});
|
||||
|
||||
function createSelectionModel(memoryIds: string[], prompts: string[] = []): StructuredModelAdapter {
|
||||
return {
|
||||
provider: 'fake-structured',
|
||||
model: 'selection-model',
|
||||
async generateObject<TOutput>(request: { prompt: string }): Promise<TOutput> {
|
||||
prompts.push(request.prompt);
|
||||
return { memoryIds } satisfies ConversationMemorySelectionResult as TOutput;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseModel(plan: ConversationTurnPlan, prompts: string[] = []): StructuredModelAdapter {
|
||||
return {
|
||||
provider: 'fake-structured',
|
||||
model: 'response-model',
|
||||
async generateObject<TOutput>(request: { prompt: string }): Promise<TOutput> {
|
||||
prompts.push(request.prompt);
|
||||
return plan as TOutput;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function seedPersonaMemory(db: Awaited<ReturnType<typeof createDb>>) {
|
||||
await db.upsertSpace({
|
||||
name: 'persona:minji',
|
||||
metadata: { boxbrain: { domain: 'persona.space', personaId: 'minji', displayName: 'Minji' } },
|
||||
});
|
||||
|
||||
await persistFactDrafts(db, {
|
||||
spaceName: 'persona:minji',
|
||||
domain: 'persona.biography',
|
||||
source: 'boxbrain:test',
|
||||
facts: [
|
||||
{
|
||||
statement: 'Minji prefers quiet evenings and close family dinners.',
|
||||
topics: [{ name: 'Minji' }, { name: 'family dinner' }],
|
||||
},
|
||||
{
|
||||
statement: 'Minji knows Shinwoo as a close contact she messages casually.',
|
||||
topics: [{ name: 'Minji' }, { name: 'Shinwoo' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await generateSchedule(db, {
|
||||
spaceName: 'persona:minji',
|
||||
displayName: 'Minji',
|
||||
currentDate: '2026-05-12',
|
||||
scope: 'day',
|
||||
structuredModel: {
|
||||
provider: 'fake-structured',
|
||||
model: 'schedule-model',
|
||||
async generateObject<TOutput>(): Promise<TOutput> {
|
||||
return {
|
||||
events: [
|
||||
{
|
||||
title: 'Family dinner',
|
||||
description: 'Dinner at home with family.',
|
||||
startAt: '2026-05-12T18:00:00.000Z',
|
||||
endAt: '2026-05-12T20:00:00.000Z',
|
||||
availabilityMode: 'offline',
|
||||
availabilityReason: 'family dinner',
|
||||
kind: 'special',
|
||||
},
|
||||
],
|
||||
} satisfies ScheduleGenerationResult as TOutput;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
16
tests/helpers.ts
Normal file
16
tests/helpers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IdentityDB } from 'identitydb';
|
||||
|
||||
const openDbs: IdentityDB[] = [];
|
||||
|
||||
export async function createDb() {
|
||||
const db = await IdentityDB.connect({ client: 'sqlite', filename: ':memory:' });
|
||||
await db.initialize();
|
||||
openDbs.push(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function closeOpenDbs() {
|
||||
while (openDbs.length > 0) {
|
||||
await openDbs.pop()!.close();
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
createReplyDelay,
|
||||
createTypingDelay,
|
||||
generateSchedule,
|
||||
ONLINE_AVAILABILITY,
|
||||
replyToConversation,
|
||||
type BoxBrainFactDraft,
|
||||
type SpecialDateProvider,
|
||||
type TextModelAdapter,
|
||||
} from '../src';
|
||||
|
||||
@@ -34,4 +37,16 @@ describe('public API', () => {
|
||||
|
||||
expect(fact.topics.map((topic) => topic.name)).toEqual(['Mina', 'quiet cafés']);
|
||||
});
|
||||
|
||||
it('exports schedule, conversation, and external special-date adapter contracts', () => {
|
||||
const specialDateProvider: SpecialDateProvider = {
|
||||
async listSpecialDates() {
|
||||
return [{ date: '2026-05-08', title: 'Parents Day' }];
|
||||
},
|
||||
};
|
||||
|
||||
expect(typeof generateSchedule).toBe('function');
|
||||
expect(typeof replyToConversation).toBe('function');
|
||||
expect(specialDateProvider.listSpecialDates).toBeTypeOf('function');
|
||||
});
|
||||
});
|
||||
|
||||
178
tests/schedule.test.ts
Normal file
178
tests/schedule.test.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user