This commit is contained in:
117
tests/conversation.test.ts
Normal file
117
tests/conversation.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { InMemoryMemoryStore, Persona, type ReplyGenerationInput } from '../src';
|
||||
|
||||
describe('Conversation API', () => {
|
||||
it('loads mandatory memory, schedules, availability, and formatted history before replying', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
let captured: ReplyGenerationInput | undefined;
|
||||
const persona = new Persona('Mina', 'Mina likes quiet cafes.', {
|
||||
memory,
|
||||
now: '2026-05-01T10:00:00.000Z',
|
||||
models: {
|
||||
conversation: {
|
||||
async generateReply(input) {
|
||||
captured = input;
|
||||
return { messages: ['카페에 있었어.', '너는 뭐해?'] };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const space = await persona.ready();
|
||||
await memory.addFact(space.id, { statement: 'The user is close to Mina.', topics: ['user', 'persona'] });
|
||||
await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'study for an exam');
|
||||
|
||||
const reply = await persona.sendMessage({
|
||||
datetime: '2026-05-01T12:00:00.000Z',
|
||||
messageHistory: [
|
||||
{ sender: 'persona', time: '2026-04-30T23:00:00.000Z', content: '다음에 보자' },
|
||||
{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '지금 뭐해?' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(reply.messages).toEqual(['카페에 있었어.', '너는 뭐해?']);
|
||||
expect(captured?.context.formattedMessageHistory).toContain('Mina@2026-04-30T23:00:00.000Z: 다음에 보자');
|
||||
expect(captured?.context.formattedMessageHistory).toContain('user@2026-05-01T12:00:00.000Z: 지금 뭐해?');
|
||||
expect(captured?.context.memorySummary).toContain('The user is close to Mina.');
|
||||
expect(captured?.context.scheduleEntries.length).toBeGreaterThan(0);
|
||||
expect(captured?.context.availability.ranges.length).toBeGreaterThan(0);
|
||||
expect(captured?.instruction).toContain('send_message');
|
||||
});
|
||||
|
||||
it('explicitly tells the response model when mandatory memory is missing', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
let memorySummary = '';
|
||||
const persona = new Persona('Mina', 'Mina is new.', {
|
||||
memory,
|
||||
now: '2026-05-01T10:00:00.000Z',
|
||||
models: {
|
||||
initialization: { async extractInitialFacts() { return []; } },
|
||||
conversation: {
|
||||
async generateReply(input) {
|
||||
memorySummary = input.context.memorySummary;
|
||||
return { messages: ['잘 모르겠어.'] };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await persona.ready();
|
||||
|
||||
await persona.sendMessage({
|
||||
datetime: '2026-05-01T12:00:00.000Z',
|
||||
messageHistory: [{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '나 기억해?' }],
|
||||
});
|
||||
|
||||
expect(memorySummary).toBe('기억이 없음');
|
||||
});
|
||||
|
||||
it('lets a rewrite model discard a stale draft when new user messages arrive', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const persona = new Persona('Mina', 'Mina is concise.', {
|
||||
memory,
|
||||
now: '2026-05-01T10:00:00.000Z',
|
||||
models: {
|
||||
conversation: { async generateReply() { return { messages: ['첫 답장'] }; } },
|
||||
rewrite: {
|
||||
async decide() {
|
||||
return { rewrite: true, draft: { messages: ['새 메시지까지 보고 답장'] }, reason: 'latest user message changes intent' };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await persona.ready();
|
||||
|
||||
const reply = await persona.sendMessage({
|
||||
datetime: '2026-05-01T12:00:00.000Z',
|
||||
messageHistory: [{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '안녕' }],
|
||||
getLatestMessageHistory: async () => [
|
||||
{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '안녕' },
|
||||
{ sender: 'user', time: '2026-05-01T12:00:05.000Z', content: '아 맞다, 지금 바빠?' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(reply.messages).toEqual(['새 메시지까지 보고 답장']);
|
||||
});
|
||||
|
||||
it('can proactively start a conversation with the same mandatory context pipeline', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
let mode = '';
|
||||
const persona = new Persona('Mina', 'Mina starts soft conversations.', {
|
||||
memory,
|
||||
now: '2026-05-01T10:00:00.000Z',
|
||||
models: {
|
||||
conversation: {
|
||||
async generateReply(input) {
|
||||
mode = input.mode;
|
||||
return { messages: ['오늘 좀 조용하네.'] };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await persona.ready();
|
||||
|
||||
const started = await persona.startConversation({ datetime: '2026-05-01T20:00:00.000Z', messageHistory: [] });
|
||||
|
||||
expect(mode).toBe('start-conversation');
|
||||
expect(started.messages).toEqual(['오늘 좀 조용하네.']);
|
||||
});
|
||||
});
|
||||
43
tests/persona.test.ts
Normal file
43
tests/persona.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { InMemoryMemoryStore, Persona, type FactDraft } from '../src';
|
||||
|
||||
describe('Persona initialization', () => {
|
||||
it('creates a new isolated persona space from displayName and seed message', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const debug: string[] = [];
|
||||
const persona = new Persona('Mina', 'Mina is a careful student who likes quiet cafes.', {
|
||||
memory,
|
||||
now: '2026-05-01T10:00:00.000Z',
|
||||
debug: (event) => { debug.push(event.name); },
|
||||
models: {
|
||||
initialization: {
|
||||
async extractInitialFacts(input): Promise<FactDraft[]> {
|
||||
return [
|
||||
{
|
||||
statement: `${input.displayName} likes quiet cafes.`,
|
||||
topics: ['persona', input.displayName],
|
||||
source: 'test',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const space = await persona.ready();
|
||||
expect(space.displayName).toBe('Mina');
|
||||
expect(space.id).toMatch(/^persona-mina-/);
|
||||
expect(await memory.findFacts(space.id, ['persona'])).toHaveLength(1);
|
||||
expect(debug).toContain('persona.initialized');
|
||||
});
|
||||
|
||||
it('loads an existing persona space by space id without creating another space', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const created = new Persona('Joon', 'Joon is a freelance designer.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||
const space = await created.ready();
|
||||
|
||||
const loaded = new Persona(space.id, { memory, now: '2026-05-01T11:00:00.000Z' });
|
||||
await expect(loaded.ready()).resolves.toMatchObject({ id: space.id, displayName: 'Joon' });
|
||||
expect(memory.spaces.size).toBe(1);
|
||||
});
|
||||
});
|
||||
50
tests/schedule.test.ts
Normal file
50
tests/schedule.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { InMemoryMemoryStore, Persona } from '../src';
|
||||
|
||||
describe('Persona schedules and availability', () => {
|
||||
it('creates tomorrow as a ten-minute daily schedule and persists it in memory', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const persona = new Persona('Mina', 'Mina works weekdays and studies at night.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||
const space = await persona.ready();
|
||||
|
||||
const entries = await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.');
|
||||
|
||||
expect(entries).toHaveLength(144);
|
||||
expect(entries[0]).toMatchObject({
|
||||
spaceId: space.id,
|
||||
startAt: '2026-05-02T00:00:00.000Z',
|
||||
endAt: '2026-05-02T00:10:00.000Z',
|
||||
granularity: 'ten-minute',
|
||||
});
|
||||
expect(entries.at(-1)?.endAt).toBe('2026-05-03T00:00:00.000Z');
|
||||
await expect(memory.listScheduleEntries(space.id, '2026-05-02T00:00:00.000Z', '2026-05-03T00:00:00.000Z')).resolves.toHaveLength(144);
|
||||
});
|
||||
|
||||
it('derives online, do-not-disturb, and offline availability from the in-memory schedule window', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const persona = new Persona('Mina', 'Mina works weekdays and studies at night.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||
await persona.ready();
|
||||
|
||||
await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.');
|
||||
const availability = await persona.getTodayScheduledAvailability('2026-05-01T12:00:00.000Z');
|
||||
|
||||
expect(availability.windowStartAt).toBe('2026-05-01T00:00:00.000Z');
|
||||
expect(availability.windowEndAt).toBe('2026-05-03T00:00:00.000Z');
|
||||
expect(new Set(availability.ranges.map((range) => range.mode))).toEqual(new Set(['offline', 'online', 'do-not-disturb']));
|
||||
expect(availability.ranges.find((range) => range.mode === 'offline')?.startAt).toBe('2026-05-02T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('prunes schedule entries before a caller-provided cutoff', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const persona = new Persona('Mina', 'Mina works weekdays.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||
const space = await persona.ready();
|
||||
await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.');
|
||||
|
||||
const deleted = await persona.deleteSchedulesBefore('2026-05-02T12:00:00.000Z');
|
||||
|
||||
expect(deleted).toBe(72);
|
||||
await expect(memory.listScheduleEntries(space.id, '2026-05-02T00:00:00.000Z', '2026-05-03T00:00:00.000Z')).resolves.toHaveLength(72);
|
||||
const deletionFacts = await memory.findFacts(space.id, ['persona.schedule.deleted']);
|
||||
expect(deletionFacts[0]?.metadata?.['deleted']).toBe(72);
|
||||
});
|
||||
});
|
||||
38
tests/sleep-memory.test.ts
Normal file
38
tests/sleep-memory.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { InMemoryMemoryStore, Persona } from '../src';
|
||||
|
||||
describe('sleepMemory', () => {
|
||||
it('objectivizes and persists extracted durable facts through the memory model', async () => {
|
||||
const memory = new InMemoryMemoryStore();
|
||||
const persona = new Persona('Mina', 'Mina remembers stable details.', {
|
||||
memory,
|
||||
now: '2026-05-01T10:00:00.000Z',
|
||||
models: {
|
||||
memoryExtraction: {
|
||||
async extract(input) {
|
||||
expect(input.formattedMessageHistory).toContain('user@2026-05-01T15:00:00.000Z: 나는 타입스크립트를 2025년부터 시작했어');
|
||||
expect(input.instruction).toContain('Objectivize');
|
||||
return [
|
||||
{
|
||||
statement: 'The user started TypeScript in 2025.',
|
||||
topics: ['user', 'TypeScript', '2025'],
|
||||
confidence: 0.9,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const space = await persona.ready();
|
||||
|
||||
const drafts = await persona.sleepMemory({
|
||||
datetime: '2026-05-02T00:00:00.000Z',
|
||||
messageHistory: [{ sender: 'user', time: '2026-05-01T15:00:00.000Z', content: '나는 타입스크립트를 2025년부터 시작했어' }],
|
||||
});
|
||||
|
||||
expect(drafts).toHaveLength(1);
|
||||
const facts = await memory.findFacts(space.id, ['TypeScript']);
|
||||
expect(facts[0]).toMatchObject({ statement: 'The user started TypeScript in 2025.', source: 'boxbrain.sleepMemory' });
|
||||
expect(facts[0]?.topics).toContain('sleepMemory');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user