import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, rmSync, symlinkSync } from 'node:fs'; import { join } from 'node:path'; import * as api from '../src/index.mjs'; import { git, prdFixture } from './prd-fixture.mjs'; test('candidate reads exact committed blobs and tolerates unrelated HEAD commits', async t => { const f = await prdFixture(t), candidate = await api.readPrdCandidate(f.args); assert.equal(candidate.prd_blob, git(f.repo, 'rev-parse', `${f.args.records_commit}:docs/prd-1.0.md`)); assert.equal(candidate.requirements[0].blob, git(f.repo, 'rev-parse', `${f.args.records_commit}:requirements/REQ-POMO-1.json`)); assert.equal(candidate.spec_commit, git(f.repo, 'log', '-1', '--format=%H', '--', 'spec/north-star.md')); assert.deepEqual(Object.keys(candidate).sort(), ['version','request','project','prd_version','records_commit','prd_path','prd_blob','requirements','spec_commit','mode_revision'].sort()); f.save('docs/unrelated.md', 'Unrelated'); assert.deepEqual(await api.readPrdCandidate(f.args), candidate); }); for (const path of ['docs/prd-1.0.md', 'requirements/REQ-POMO-1.json']) test(`changed selected blob is stale: ${path}`, async t => { const f = await prdFixture(t); f.save(path, path.endsWith('.md') ? 'Edited' : JSON.stringify({ recordVersion: 1, id: 'REQ-POMO-1', title: 'Edited', text: 'Changed', status: 'accepted' })); await assert.rejects(api.readPrdCandidate(f.args), { code: 'prd-candidate-stale' }); }); test('candidate rejects extra assertions, duplicate IDs, wrong version and unreachable commits', async t => { const f = await prdFixture(t); for (const patch of [{ prd_blob: 'forged' }, { requirement_ids: ['REQ-POMO-1','REQ-POMO-1'] }, { prd_version: '2.0' }, { records_commit: 'a'.repeat(40) }]) await assert.rejects(api.readPrdCandidate({ ...f.args, ...patch })); await assert.rejects(api.readProjectFile({ ...f.context, path: 'requirements/REQ-POMO-1.json' }), { code: 'project-read-path' }); }); test('candidate rejects malformed requirements, mismatched IDs and nonregular blobs', async t => { const f = await prdFixture(t); const original = JSON.parse(readFileSync(join(f.repo, 'requirements/REQ-POMO-1.json'))); for (const record of [{ ...original, extra: true }, { ...original, id: 'OTHER' }, { ...original, title: ' ' }]) { const records_commit = f.save('requirements/REQ-POMO-1.json', JSON.stringify(record), 'pm.pm'); await assert.rejects(api.readPrdCandidate({ ...f.args, records_commit })); } rmSync(join(f.repo, 'docs/prd-1.0.md')); symlinkSync('../spec/north-star.md', join(f.repo, 'docs/prd-1.0.md')); git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'Link'); await assert.rejects(api.readPrdCandidate({ ...f.args, records_commit: git(f.repo, 'rev-parse', 'HEAD') })); }); test('candidate enforces 64 requirements, 32 KiB PRD, 16 KiB requirement and 256 KiB aggregate', async t => { const f = await prdFixture(t); await assert.rejects(api.readPrdCandidate({ ...f.args, requirement_ids: Array.from({ length: 65 }, (_, i) => `R${i}`) })); let records_commit = f.save('docs/prd-1.0.md', 'x'.repeat(32769)); await assert.rejects(api.readPrdCandidate({ ...f.args, records_commit }), { code: 'prd-candidate-size' }); f.save('docs/prd-1.0.md', 'x'.repeat(32768), 'pm.pm'); const record = { recordVersion: 1, id: 'REQ-POMO-1', title: 'Timer', text: 'x'.repeat(16385), status: 'draft' }; records_commit = f.save('requirements/REQ-POMO-1.json', JSON.stringify(record), 'pm.pm'); await assert.rejects(api.readPrdCandidate({ ...f.args, records_commit }), { code: 'prd-candidate-size' }); const ids = Array.from({ length: 16 }, (_, i) => `R${i}`); for (const id of ids) f.save(`requirements/${id}.json`, JSON.stringify({ ...record, id, text: 'x'.repeat(16384) }), 'pm.pm'); await assert.rejects(api.readPrdCandidate({ ...f.args, records_commit: git(f.repo, 'rev-parse', 'HEAD'), requirement_ids: ids }), { code: 'prd-candidate-size' }); });