77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
/**
|
|
* Tests for the Instructor thread memory compression.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
compressIfNeeded,
|
|
COMPRESS_THRESHOLD,
|
|
MAX_EARLIER_SUMMARY_CHARS,
|
|
} from '@/lib/pbl/v2/agents/instructor-memory';
|
|
import type { PBLAgentThread, PBLChatMessage } from '@/lib/pbl/v2/types';
|
|
|
|
function makeThread(messageCount: number): PBLAgentThread {
|
|
const messages: PBLChatMessage[] = [];
|
|
for (let i = 0; i < messageCount; i++) {
|
|
messages.push({
|
|
id: `msg-${i}`,
|
|
roleType: i % 2 === 0 ? 'user' : 'instructor',
|
|
content: i % 2 === 0 ? `user msg #${i}` : `instructor reply #${i}`,
|
|
ts: new Date(Date.UTC(2026, 4, 26, 12, 0, i)).toISOString(),
|
|
});
|
|
}
|
|
return { agentId: 'role-instructor', messages };
|
|
}
|
|
|
|
describe('PBL v2 — instructor memory compression', () => {
|
|
it('returns the thread unchanged when below threshold', () => {
|
|
const thread = makeThread(COMPRESS_THRESHOLD - 1);
|
|
const out = compressIfNeeded(thread);
|
|
expect(out).toBe(thread); // same reference, no work done
|
|
});
|
|
|
|
it('compresses when above threshold', () => {
|
|
const thread = makeThread(COMPRESS_THRESHOLD + 10);
|
|
const out = compressIfNeeded(thread);
|
|
expect(out.messages.length).toBeLessThan(thread.messages.length);
|
|
expect(out.earlierSummary).toBeDefined();
|
|
expect(out.earlierSummary).toContain('Earlier conversation digest');
|
|
});
|
|
|
|
it('cuts on a user-message boundary so a turn pair is never split', () => {
|
|
const thread = makeThread(COMPRESS_THRESHOLD + 10);
|
|
const out = compressIfNeeded(thread);
|
|
// After cut, the first surviving message should be a user message
|
|
// (digestor invariant).
|
|
if (out.messages.length > 0) {
|
|
expect(out.messages[0].roleType).toBe('user');
|
|
}
|
|
});
|
|
|
|
it('concatenates with prior earlierSummary instead of replacing it', () => {
|
|
const thread = makeThread(COMPRESS_THRESHOLD + 10);
|
|
thread.earlierSummary = 'PRIOR_SUMMARY_MARKER';
|
|
const out = compressIfNeeded(thread);
|
|
expect(out.earlierSummary).toContain('PRIOR_SUMMARY_MARKER');
|
|
expect(out.earlierSummary).toContain('Earlier conversation digest');
|
|
});
|
|
|
|
it('extracts durable learner context from folded turns', () => {
|
|
const thread = makeThread(COMPRESS_THRESHOLD + 10);
|
|
thread.messages[4].content = '我是零基础,想最简单一点。';
|
|
thread.messages[8].content = '我用 VS Code 和终端,Python 已经装好了。';
|
|
thread.messages[12].content = '这里报错了,我看不懂 traceback。';
|
|
const out = compressIfNeeded(thread);
|
|
expect(out.earlierSummary).toContain('Learner context inferred');
|
|
expect(out.earlierSummary).toContain('Level/preference');
|
|
expect(out.earlierSummary).toContain('Environment/tools');
|
|
expect(out.earlierSummary).toContain('Blocker/confusion');
|
|
});
|
|
|
|
it('keeps folded memory bounded across repeated compressions', () => {
|
|
let thread = makeThread(COMPRESS_THRESHOLD + 10);
|
|
thread.earlierSummary = 'x'.repeat(MAX_EARLIER_SUMMARY_CHARS + 2000);
|
|
thread = compressIfNeeded(thread);
|
|
expect(thread.earlierSummary!.length).toBeLessThanOrEqual(MAX_EARLIER_SUMMARY_CHARS + 120);
|
|
});
|
|
});
|