/** * Replay Buffer 单元测试(v0.8.1 P0-3 模块化收口) * * 锁定有界缓冲契约:条数/字节双上限溢出丢最旧、truncated 标记、runId 记录、 * TERMINATED 保留 + 新内容兜底重置、终态清除(会话删除联动)。 */ import { describe, it, expect, beforeEach } from 'vitest'; import { appendReplay, resetReplayBuffer, markReplayTerminated, clearReplayBuffer, getReplayBufferData, } from '../replay-buffer'; describe('replay-buffer — 有界回放缓冲(v0.8.1 P0-3)', () => { beforeEach(() => { clearReplayBuffer('s1'); }); it('append + get:按序返回事件与 runId', () => { appendReplay('s1', { channel: 'stateChange', payload: { runId: 'run_a' }, ts: 1 }); appendReplay('s1', { channel: 'streamEvent', payload: { type: 'text_delta' }, ts: 2 }); const data = getReplayBufferData('s1'); expect(data.events).toHaveLength(2); expect(data.events[0].channel).toBe('stateChange'); expect(data.runId).toBe('run_a'); expect(data.truncated).toBe(false); }); it('条数上限:超过 2000 条丢弃最旧并置 truncated', () => { for (let i = 0; i < 2005; i++) { appendReplay('s1', { channel: 'streamEvent', payload: { i }, ts: i }); } const data = getReplayBufferData('s1'); expect(data.events.length).toBeLessThanOrEqual(2000); expect(data.truncated).toBe(true); // 最旧的已被丢弃 expect((data.events[0].payload as { i: number }).i).toBeGreaterThan(0); }); it('字节上限:单条 1MB 的超大 payload 被丢弃并置 truncated', () => { const big = 'x'.repeat(1024 * 1024); for (let i = 0; i < 6; i++) { appendReplay('s1', { channel: 'streamEvent', payload: { big }, ts: i }); } const data = getReplayBufferData('s1'); expect(data.truncated).toBe(true); // 缓冲字节被控制在 4MB 上限附近(至少留最后一条) expect(data.events.length).toBeGreaterThanOrEqual(1); expect(data.events.length).toBeLessThan(6); }); it('TERMINATED 保留缓冲;新内容到达兜底重置', () => { appendReplay('s1', { channel: 'stateChange', payload: { state: 'TERMINATED' }, ts: 1 }); markReplayTerminated('s1'); // 完成后仍可回放最终内容 expect(getReplayBufferData('s1').events).toHaveLength(1); // 下一次 run 的新内容 → 缓冲重置 appendReplay('s1', { channel: 'streamEvent', payload: { type: 'text_delta' }, ts: 2 }); const data = getReplayBufferData('s1'); expect(data.events).toHaveLength(1); expect(data.truncated).toBe(false); }); it('INIT 重置:清除上一 run 的缓冲', () => { appendReplay('s1', { channel: 'streamEvent', payload: { a: 1 }, ts: 1 }); resetReplayBuffer('s1'); expect(getReplayBufferData('s1').events).toHaveLength(0); }); it('clearReplayBuffer:终态清除(会话删除联动)', () => { appendReplay('s1', { channel: 'streamEvent', payload: { a: 1 }, ts: 1 }); clearReplayBuffer('s1'); const data = getReplayBufferData('s1'); expect(data.events).toHaveLength(0); expect(data.runId).toBeNull(); }); });