timeline-logger.test.js 3.62 KB
/**
 * 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);
  });
});