import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { extractManifestYaml, parseManifest, validateAllManifests, } from '../../lib/parsers/manifest-yaml.mjs'; const STEP_BODY_GOOD = `### Step 1: Add validator - Files: lib/foo.mjs - Verify: \`npm test\` → expected: pass - Checkpoint: \`git commit -m "feat(lib): foo"\` - Manifest: \`\`\`yaml manifest: expected_paths: - lib/foo.mjs min_file_count: 1 commit_message_pattern: "^feat\\\\(lib\\\\):" bash_syntax_check: [] forbidden_paths: [] must_contain: [] \`\`\` `; const STEP_BODY_NO_MANIFEST = `### Step 1: oops no manifest here `; const STEP_BODY_INVALID_REGEX = `### Step 1: bad regex - Manifest: \`\`\`yaml manifest: expected_paths: - x min_file_count: 1 commit_message_pattern: "[unclosed" bash_syntax_check: [] forbidden_paths: [] must_contain: [] \`\`\` `; test('extractManifestYaml — finds fenced manifest block', () => { const yaml = extractManifestYaml(STEP_BODY_GOOD); assert.ok(yaml); assert.match(yaml, /expected_paths/); }); test('extractManifestYaml — null when missing', () => { assert.equal(extractManifestYaml(STEP_BODY_NO_MANIFEST), null); }); test('parseManifest — happy path produces all required keys', () => { const r = parseManifest(STEP_BODY_GOOD); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.deepEqual(r.parsed.expected_paths, ['lib/foo.mjs']); assert.equal(r.parsed.min_file_count, 1); assert.match(r.parsed.commit_message_pattern, /^\^feat/); }); test('parseManifest — missing manifest produces MANIFEST_MISSING', () => { const r = parseManifest(STEP_BODY_NO_MANIFEST); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'MANIFEST_MISSING')); }); test('parseManifest — invalid regex caught', () => { const r = parseManifest(STEP_BODY_INVALID_REGEX); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'MANIFEST_PATTERN_INVALID')); }); test('parseManifest — missing required key flagged', () => { const noCount = `### Step 1 - Manifest: \`\`\`yaml manifest: expected_paths: - x commit_message_pattern: "^x:" bash_syntax_check: [] forbidden_paths: [] must_contain: [] \`\`\` `; const r = parseManifest(noCount); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'MANIFEST_MISSING_KEY' && /min_file_count/.test(e.message))); }); test('parseManifest — commit_message_pattern compiles via new RegExp', () => { const r = parseManifest(STEP_BODY_GOOD); const re = new RegExp(r.parsed.commit_message_pattern); assert.ok(re.test('feat(lib): added foo')); assert.ok(!re.test('chore: not it')); }); test('parseManifest — must_contain list-of-dicts (real-world template form)', () => { const body = `### Step 1: Real - Manifest: \`\`\`yaml manifest: expected_paths: - a.json - b.md min_file_count: 2 commit_message_pattern: "^chore:" bash_syntax_check: [] forbidden_paths: - CHANGELOG.md must_contain: - path: a.json pattern: '"version": "2\\.3\\.0"' - path: b.md pattern: "version-blue" \`\`\` `; const r = parseManifest(body); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.equal(r.parsed.must_contain.length, 2); assert.equal(r.parsed.must_contain[0].path, 'a.json'); assert.equal(r.parsed.must_contain[1].path, 'b.md'); assert.equal(r.parsed.forbidden_paths[0], 'CHANGELOG.md'); }); test('validateAllManifests — aggregates per-step issues', () => { const steps = [ { n: 1, body: STEP_BODY_GOOD }, { n: 2, body: STEP_BODY_NO_MANIFEST }, ]; const r = validateAllManifests(steps); assert.equal(r.valid, false); assert.ok(r.errors.find(e => /Step 2/.test(e.message))); });