/** * timelineLogger + changelogWriter 单测 */ const fs = require('fs'); const pathResolver = require('../../services/devloop/path-resolver'); const timelineLogger = require('../../services/devloop/timeline-logger'); const changelogWriter = require('../../services/devloop/changelog-writer'); describe('timelineLogger', () => { let pid, root; beforeEach(() => { pathResolver._resetForTesting(); const r = pathResolver.allocateProjectRoot({ company_id: 'c-test', mode: 'sandbox' }); pid = r.pid; root = r.root_path; }); afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); it('append writes NDJSON line', () => { timelineLogger.append(pid, { source: 'system', kind: 'project_created', ref: pid, summary: 'test project', }); const events = timelineLogger.read(pid); expect(events).toHaveLength(1); expect(events[0].kind).toBe('project_created'); }); it('append rejects missing fields', () => { expect(() => timelineLogger.append(pid, {})).toThrow(); expect(() => timelineLogger.append(pid, { source: 'x' })).toThrow(); }); it('summary truncated to 280 chars', () => { const longSummary = 'x'.repeat(500); timelineLogger.append(pid, { source: 'system', kind: 'test', summary: longSummary, }); const events = timelineLogger.read(pid); expect(events[0].summary.length).toBeLessThanOrEqual(280); expect(events[0].summary).toMatch(/…$/); }); it('oversized payload reduced to ref marker', () => { const big = { huge: 'x'.repeat(5000) }; timelineLogger.append(pid, { source: 'system', kind: 'test', summary: 's', payload: big, }); const events = timelineLogger.read(pid); expect(events[0].payload.oversize).toBe(true); }); it('tail returns last N', () => { for (let i = 0; i < 15; i++) { timelineLogger.append(pid, { source: 'system', kind: 'k', summary: `${i}` }); } const last5 = timelineLogger.tail(pid, 5); expect(last5).toHaveLength(5); // 时间正序,最后一条是最新 expect(last5[4].summary).toBe('14'); }); it('filter by kind', () => { timelineLogger.append(pid, { source: 'pm', kind: 'prd_drafted', summary: 'a' }); timelineLogger.append(pid, { source: 'arch', kind: 'sdd_generated', summary: 'b' }); timelineLogger.append(pid, { source: 'pm', kind: 'prd_drafted', summary: 'c' }); const r = timelineLogger.read(pid, { kind: 'prd_drafted' }); expect(r).toHaveLength(2); }); }); describe('changelogWriter', () => { let pid, root; beforeEach(() => { pathResolver._resetForTesting(); const r = pathResolver.allocateProjectRoot({ company_id: 'c-test', mode: 'sandbox' }); pid = r.pid; root = r.root_path; }); afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); it('append writes entry', () => { changelogWriter.append(pid, { kind: 'prd_rev', rev: 1, by: 'pm-agent', trigger: 'init', summary: 'first PRD', }); const all = changelogWriter.read(pid); expect(all).toHaveLength(1); expect(all[0].kind).toBe('prd_rev'); expect(all[0].rev).toBe(1); }); it('rejects invalid kind', () => { expect(() => changelogWriter.append(pid, { kind: 'wrong-kind', rev: 1, summary: 's', })).toThrow(); }); it('latest returns most recent of kind', () => { changelogWriter.append(pid, { kind: 'prd_rev', rev: 1, summary: 's1' }); changelogWriter.append(pid, { kind: 'sdd_rev', rev: 1, summary: 's2' }); changelogWriter.append(pid, { kind: 'prd_rev', rev: 2, summary: 's3' }); const r = changelogWriter.latest(pid, 'prd_rev'); expect(r.rev).toBe(2); }); });