doc-store.test.js 4.41 KB
/**
 * docStore 单测
 */
const docStore = require('../../services/devloop/doc-store');

describe('docStore', () => {
  describe('validateHTML', () => {
    it('rejects empty', () => {
      const r = docStore.validateHTML('', 'prd');
      expect(r.ok).toBe(false);
    });

    it('rejects missing article', () => {
      const r = docStore.validateHTML('<p>hi</p>', 'prd');
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('article');
    });

    it('rejects wrong doc kind', () => {
      const r = docStore.validateHTML('<article data-doc="sdd" data-rev="1"><section><h3>x</h3><p>y</p></section></article>', 'prd');
      expect(r.ok).toBe(false);
    });

    it('accepts valid PRD chapter', () => {
      const html = `<article data-doc="prd" data-rev="2">
        <section data-rev="1"><h3>Title</h3><p>body</p></section>
        <section data-rev="2"><h3>Other</h3><p>more</p></section>
      </article>`;
      const r = docStore.validateHTML(html, 'prd');
      expect(r.ok).toBe(true);
    });

    it('detects section without h3', () => {
      const html = `<article data-doc="prd" data-rev="1">
        <section><p>only p</p></section>
      </article>`;
      const r = docStore.validateHTML(html, 'prd');
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('h3');
    });

    it('detects non-monotonic data-rev', () => {
      const html = `<article data-doc="prd" data-rev="3">
        <section data-rev="2"><h3>x</h3><p>y</p></section>
        <section data-rev="1"><h3>x</h3><p>y</p></section>
      </article>`;
      const r = docStore.validateHTML(html, 'prd');
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('monotonic');
    });
  });

  describe('validateUnitsSchema', () => {
    const validUnit = {
      id: 'auth-be', type: 'be', status: 'pending',
      depends_on: [], retries: 0, retry_budget: 5,
      contract_hash: 'sha256:abc',
    };

    it('accepts valid units.json', () => {
      const json = {
        project_id: 'p-test', prd_rev: '1', sdd_rev: '1',
        units: [validUnit], l2_rounds: 0, l2_budget: 3,
      };
      const r = docStore.validateUnitsSchema(json);
      expect(r.ok).toBe(true);
    });

    it('rejects invalid unit id', () => {
      const json = {
        project_id: 'p', units: [{ ...validUnit, id: 'AuthBE' }],
      };
      const r = docStore.validateUnitsSchema(json);
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('naming');
    });

    it('rejects duplicate ids', () => {
      const json = {
        project_id: 'p', units: [validUnit, validUnit],
      };
      const r = docStore.validateUnitsSchema(json);
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('duplicate');
    });

    it('rejects unknown depends_on', () => {
      const json = {
        project_id: 'p',
        units: [{ ...validUnit, depends_on: ['ghost-be'] }],
      };
      const r = docStore.validateUnitsSchema(json);
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('unknown unit');
    });

    it('detects cycle', () => {
      const a = { id: 'a-be', type: 'be', status: 'pending', depends_on: ['b-be'], retries: 0, retry_budget: 5, contract_hash: 'sha256:1' };
      const b = { id: 'b-be', type: 'be', status: 'pending', depends_on: ['a-be'], retries: 0, retry_budget: 5, contract_hash: 'sha256:2' };
      const json = { project_id: 'p', units: [a, b] };
      const r = docStore.validateUnitsSchema(json);
      expect(r.ok).toBe(false);
      expect(r.errors.join(' ')).toContain('cycle');
    });
  });

  describe('hashContract', () => {
    it('deterministic for same content', () => {
      const c = { unit_id: 'x', exports: { http: [{ path: '/x' }] } };
      const h1 = docStore.hashContract(c);
      const h2 = docStore.hashContract(c);
      expect(h1).toBe(h2);
      expect(h1).toMatch(/^sha256:[a-f0-9]{16}$/);
    });

    it('different content → different hash', () => {
      const c1 = { unit_id: 'x', exports: { http: [{ path: '/x' }] } };
      const c2 = { unit_id: 'x', exports: { http: [{ path: '/y' }] } };
      expect(docStore.hashContract(c1)).not.toBe(docStore.hashContract(c2));
    });

    it('canonical: key ordering does not affect hash', () => {
      const c1 = { exports: { http: [{ a: 1, b: 2 }] } };
      const c2 = { exports: { http: [{ b: 2, a: 1 }] } };
      expect(docStore.hashContract(c1)).toBe(docStore.hashContract(c2));
    });
  });
});