Files
thu-maic--openmaic/tests/pbl/v2/instructor-memory.test.ts
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

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