Files
BoxBrain/tests/persona.test.ts

290 lines
9.2 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import { IdentityDB } from 'identitydb';
import { initializePersona, type PersonaBiographyResult, type PersonaFactExtractionResult } from '../src';
import type { ImageModelAdapter, StructuredModelAdapter } from '../src';
const openDbs: IdentityDB[] = [];
async function createDb() {
const db = await IdentityDB.connect({ client: 'sqlite', filename: ':memory:' });
await db.initialize();
openDbs.push(db);
return db;
}
afterEach(async () => {
while (openDbs.length > 0) {
await openDbs.pop()!.close();
}
});
describe('initializePersona', () => {
it('generates a biography, extracts facts, and stores them in the persona space', async () => {
const db = await createDb();
const prompts: string[] = [];
const structured = createStructuredAdapter(prompts);
const persona = await initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet, playful, observant.',
history: 'Grew up near Busan and studied design.',
values: ['loyalty', 'creative honesty'],
likes: ['quiet cafés'],
dislikes: ['loud arguments'],
relationships: [{ name: 'Haru', relationship: 'best friend', description: 'childhood friend' }],
currentDate: '2026-05-11',
structuredModel: structured,
});
expect(persona).toMatchObject({
id: 'minji',
displayName: 'Minji',
spaceName: 'persona:minji',
biography: 'Minji grew up near the sea and values loyal friendships.',
});
expect(prompts[0]).toContain('Quiet, playful, observant.');
expect(prompts[0]).toContain('creative honesty');
expect(prompts[0]).toContain('Haru');
expect(prompts[0]).toContain('Current date: 2026-05-11');
expect(prompts[1]).toContain('Minji grew up near the sea');
const facts = await db.getTopicFacts('Minji', { spaceName: persona.spaceName });
expect(facts.map((fact) => fact.statement)).toContain('Minji grew up near the sea.');
});
it('accepts a single freeform persona seed string and forwards it to the biography generation step', async () => {
const db = await createDb();
const prompts: string[] = [];
const structured = createStructuredAdapter(prompts);
const seedText =
'Mina is 29, grew up in Busan, moved to Seoul for product design work, values loyalty and quiet consistency, loves indie music and late-night walks, dislikes loud restaurants, and is very close to her older brother Jisoo.';
const persona = await initializePersona(db, {
id: 'mina',
displayName: 'Mina',
seedText,
currentDate: '2026-05-11',
structuredModel: structured,
});
expect(persona).toMatchObject({
id: 'mina',
displayName: 'Mina',
spaceName: 'persona:mina',
});
expect(prompts[0]).toContain('Freeform persona seed');
expect(prompts[0]).toContain(seedText);
expect(prompts[0]).toContain('infer personality, history, values, likes, dislikes, and relationships');
expect(prompts[0]).toContain('Current date: 2026-05-11');
});
it('does not call the image adapter unless a profile image is requested', async () => {
const db = await createDb();
const structured = createStructuredAdapter([]);
let imageCalls = 0;
const imageModel: ImageModelAdapter = {
provider: 'fake-image',
model: 'fake-image-model',
async generateImage() {
imageCalls += 1;
return { url: 'https://example.test/minji.png' };
},
};
const persona = await initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: structured,
imageModel,
});
expect(persona.profileImageUrl).toBeUndefined();
expect(imageCalls).toBe(0);
});
it('can generate and store a persona profile image when requested', async () => {
const db = await createDb();
const structured = createStructuredAdapter([]);
let imagePrompt = '';
const imageModel: ImageModelAdapter = {
provider: 'fake-image',
model: 'fake-image-model',
async generateImage(request) {
imagePrompt = request.prompt;
return { url: 'https://example.test/minji.png' };
},
};
const persona = await initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: structured,
imageModel,
generateProfileImage: true,
});
expect(imagePrompt).toContain('Minji');
expect(persona.profileImageUrl).toBe('https://example.test/minji.png');
const facts = await db.getTopicFacts('profile image', { spaceName: persona.spaceName });
expect(facts.map((fact) => fact.statement)).toContain('Minji has a generated profile image at https://example.test/minji.png.');
});
it('generates unique default persona spaces when no id is supplied', async () => {
const db = await createDb();
const first = await initializePersona(db, {
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: createStructuredAdapter([]),
});
const second = await initializePersona(db, {
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: createStructuredAdapter([]),
});
expect(first.id).not.toBe(second.id);
expect(first.spaceName).not.toBe(second.spaceName);
});
it('rejects accidental reuse of an existing persona space by default', async () => {
const db = await createDb();
await initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: createStructuredAdapter([]),
});
await expect(
initializePersona(db, {
id: 'minji',
displayName: 'Minji Again',
personality: 'Different.',
structuredModel: createStructuredAdapter([]),
}),
).rejects.toThrow(/already exists/);
});
it('allows explicit reuse of an existing persona space', async () => {
const db = await createDb();
await initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: createStructuredAdapter([]),
});
const persona = await initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: createStructuredAdapter([]),
reuseExistingSpace: true,
});
expect(persona.spaceName).toBe('persona:minji');
});
it('rejects malformed structured biography output', async () => {
const db = await createDb();
const malformed: StructuredModelAdapter = {
provider: 'fake-structured',
model: 'fake-structured-model',
async generateObject<TOutput>(): Promise<TOutput> {
return { biography: '' } as TOutput;
},
};
await expect(
initializePersona(db, {
id: 'broken',
displayName: 'Broken',
personality: 'Quiet.',
structuredModel: malformed,
}),
).rejects.toThrow(/biography/);
expect(await db.getSpaceByName('persona:broken')).toBeNull();
});
it('rejects malformed extracted fact topics before creating a space', async () => {
const db = await createDb();
const malformedFacts: StructuredModelAdapter = {
provider: 'fake-structured',
model: 'fake-structured-model',
async generateObject<TOutput>(request: { prompt: string }): Promise<TOutput> {
if (request.prompt.includes('Create a concrete')) {
return { biography: 'Broken has a biography.' } as TOutput;
}
return {
facts: [
{
statement: 'Broken has a malformed topic.',
topics: [{}],
},
],
} as TOutput;
},
};
await expect(
initializePersona(db, {
id: 'broken-topic',
displayName: 'Broken Topic',
personality: 'Quiet.',
structuredModel: malformedFacts,
}),
).rejects.toThrow(/topic name/);
expect(await db.getSpaceByName('persona:broken-topic')).toBeNull();
});
it('requires an image model when profile image generation is requested', async () => {
const db = await createDb();
await expect(
initializePersona(db, {
id: 'minji',
displayName: 'Minji',
personality: 'Quiet.',
structuredModel: createStructuredAdapter([]),
generateProfileImage: true,
}),
).rejects.toThrow(/imageModel/);
});
});
function createStructuredAdapter(prompts: string[]): StructuredModelAdapter {
let call = 0;
return {
provider: 'fake-structured',
model: 'fake-structured-model',
async generateObject<TOutput>(request: { prompt: string }): Promise<TOutput> {
prompts.push(request.prompt);
call += 1;
if (call === 1) {
return {
biography: 'Minji grew up near the sea and values loyal friendships.',
} satisfies PersonaBiographyResult as TOutput;
}
return {
facts: [
{
statement: 'Minji grew up near the sea.',
topics: [{ name: 'Minji' }, { name: 'sea' }],
},
{
statement: 'Minji values loyal friendships.',
topics: [{ name: 'Minji' }, { name: 'loyal friendships' }],
},
],
} satisfies PersonaFactExtractionResult as TOutput;
},
};
}