Files
openmaic/OpenMAIC/tests/pbl/v2/instructor-memory.test.ts
2026-08-16 14:58:47 +08:00

77 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
});
});