/** * MSD Tools Tests - UAT Audit */ 'use strict'; const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const fc = require('./helpers/fast-check-setup.cjs'); const { runMsdTools, createTempProject, createTempDir, cleanup } = require('./helpers.cjs'); const { buildCheckpoint, CHECKPOINT_FRAMES, CHECKPOINT_LANGUAGE_ALIASES, resolveCheckpointFrame, parseDeferredItems, parseDeferredItemsWithStatus, acknowledgeDeferredItem, parseUatItems, parseUatItemsWithStats, DEFERRED_MARKER_ALT, DEFERRED_BULLET_MARKERS, parseVerificationItems, parsedEntriesFor, } = require('../msd-core/bin/lib/uat.cjs'); const { iterateBullets } = require('../msd-core/bin/lib/markdown-sectionizer.cjs'); describe('audit-uat command', () => { let tmpDir; beforeEach(() => { tmpDir = createTempProject(); }); afterEach(() => { cleanup(tmpDir); }); test('returns empty results when no UAT files exist', () => { // Create a phase directory with no UAT files fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '01-foundation', '.gitkeep'), ''); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.deepStrictEqual(output.results, []); assert.strictEqual(output.summary.total_items, 0); assert.strictEqual(output.summary.total_files, 0); }); test('detects UAT with pending items', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), `--- status: testing phase: 01-foundation started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. Login Form expected: Form displays with email and password fields result: pass ### 2. Submit Button expected: Submitting shows loading state result: pending `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1); assert.strictEqual(output.results[0].phase, '01'); assert.strictEqual(output.results[0].items[0].result, 'pending'); assert.strictEqual(output.results[0].items[0].category, 'pending'); assert.strictEqual(output.results[0].items[0].name, 'Submit Button'); }); // Regression: #2273 — bracketed result values [pending], [blocked], [skipped] test('detects UAT items with bracketed result values (#2273)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: testing', 'phase: 01-foundation', 'started: 2025-01-01T00:00:00Z', 'updated: 2025-01-01T00:00:00Z', '---', '', '## Tests', '', '### 1. Login Form', 'expected: Form displays correctly', 'result: [pending]', '', '### 2. Submit Button', 'expected: Shows loading state', 'result: [blocked]', 'blocked_by: #123', '', '### 3. Error Message', 'expected: Shows validation error', 'result: [skipped]', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 3, 'all 3 bracketed items should be detected'); assert.strictEqual(output.results[0].items[0].result, 'pending', '[pending] should parse as pending'); assert.strictEqual(output.results[0].items[1].result, 'blocked', '[blocked] should parse as blocked'); assert.strictEqual(output.results[0].items[2].result, 'skipped', '[skipped] should parse as skipped'); }); test('detects UAT with blocked items and categorizes blocked_by', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '02-api'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '02-UAT.md'), `--- status: partial phase: 02-api started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. API Health Check expected: Returns 200 OK result: blocked blocked_by: server reason: Server not running locally `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1); assert.strictEqual(output.results[0].items[0].result, 'blocked'); assert.strictEqual(output.results[0].items[0].category, 'server_blocked'); assert.strictEqual(output.results[0].items[0].blocked_by, 'server'); }); test('detects false completion (complete status with pending items)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-ui'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), `--- status: complete phase: 03-ui started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. Dashboard Layout expected: Cards render in grid result: pass ### 2. Mobile Responsive expected: Grid collapses to single column on mobile result: pending `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1); assert.strictEqual(output.results[0].status, 'complete'); assert.strictEqual(output.results[0].items[0].result, 'pending'); }); test('extracts human_needed items from VERIFICATION files', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-auth'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '04-VERIFICATION.md'), `--- status: human_needed phase: 04-auth --- ## Automated Checks All passed. ## Human Verification 1. Test SSO login with Google account 2. Test password reset flow end-to-end 3. Verify MFA enrollment on new device `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 3); assert.strictEqual(output.results[0].type, 'verification'); assert.strictEqual(output.results[0].status, 'human_needed'); assert.strictEqual(output.results[0].items[0].category, 'human_uat'); assert.strictEqual(output.results[0].items[0].name, 'Test SSO login with Google account'); }); test('scans and aggregates across multiple phases', () => { // Phase 1 with pending const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phase1, { recursive: true }); fs.writeFileSync(path.join(phase1, '01-UAT.md'), `--- status: partial phase: 01-foundation started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. Test A expected: Works result: pending `); // Phase 2 with blocked const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); fs.mkdirSync(phase2, { recursive: true }); fs.writeFileSync(path.join(phase2, '02-UAT.md'), `--- status: partial phase: 02-api started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. Test B expected: Responds result: blocked blocked_by: server ### 2. Test C expected: Returns data result: skipped reason: device not available `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_files, 2); assert.strictEqual(output.summary.total_items, 3); assert.strictEqual(output.summary.by_phase['01'], 1); assert.strictEqual(output.summary.by_phase['02'], 2); }); test('milestone scoping filters phases to current milestone', () => { // Create a ROADMAP.md that only references Phase 2 fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap ### Phase 2: API Layer **Goal:** Build API `); // Phase 1 (not in current milestone) with pending const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phase1, { recursive: true }); fs.writeFileSync(path.join(phase1, '01-UAT.md'), `--- status: partial phase: 01-foundation started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. Old Test expected: Old behavior result: pending `); // Phase 2 (in current milestone) with pending const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); fs.mkdirSync(phase2, { recursive: true }); fs.writeFileSync(path.join(phase2, '02-UAT.md'), `--- status: partial phase: 02-api started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. New Test expected: New behavior result: pending `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); // Only Phase 2 should be included (Phase 1 not in ROADMAP) assert.strictEqual(output.summary.total_files, 1); assert.strictEqual(output.results[0].phase, '02'); }); test('summary by_category counts are correct', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '05-billing'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '05-UAT.md'), `--- status: partial phase: 05-billing started: 2025-01-01T00:00:00Z updated: 2025-01-01T00:00:00Z --- ## Tests ### 1. Payment Form expected: Stripe elements load result: pending ### 2. Webhook Handler expected: Processes payment events result: blocked blocked_by: third-party Stripe ### 3. Invoice PDF expected: Generates downloadable PDF result: skipped reason: needs release build ### 4. Refund Flow expected: Processes refund result: pending `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 4); assert.strictEqual(output.summary.by_category.pending, 2); assert.strictEqual(output.summary.by_category.third_party, 1); assert.strictEqual(output.summary.by_category.build_needed, 1); }); test('ignores VERIFICATION files without human_needed or gaps_found status', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-VERIFICATION.md'), `--- status: passed phase: 01-foundation --- ## Results All checks passed. `); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 0); assert.strictEqual(output.summary.total_files, 0); }); // Regression: #2383 — human_needed items with result: PASS are still reported test('ignores human_verification items with result PASS (regression #2383)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '31-auth'); fs.mkdirSync(phaseDir, { recursive: true }); // This file has status: human_needed in frontmatter but all individual items // have result: "PASS" — they should not be reported as outstanding fs.writeFileSync(path.join(phaseDir, '31-VERIFICATION.md'), [ '---', 'status: human_needed', 'phase: 31-auth', 'gaps_remaining: []', '---', '', '## Human Verification', '', '| # | Item | Result | Evidence |', '|---|------|--------|----------|', '| 1 | Test SSO login with Google | PASS | Verified 2025-01-15 |', '| 2 | Test password reset flow | PASS | Verified 2025-01-15 |', '| 3 | Verify MFA enrollment | PASS | Verified 2025-01-15 |', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 0, `Expected 0 outstanding items but got ${output.summary.total_items} — resolved PASS items should not be counted`); assert.strictEqual(output.summary.total_files, 0); }); test('ignores human_needed VERIFICATION file when file-level status is passed (regression #2383)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '31-auth'); fs.mkdirSync(phaseDir, { recursive: true }); // When the frontmatter status is "passed", skip entirely regardless of section content fs.writeFileSync(path.join(phaseDir, '31-VERIFICATION.md'), [ '---', 'status: passed', 'phase: 31-auth', 'gaps_remaining: []', '---', '', '## Human Verification', '', '1. Test SSO login with Google account', '2. Test password reset flow end-to-end', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 0, `status: passed file should produce 0 outstanding items, got ${output.summary.total_items}`); assert.strictEqual(output.summary.total_files, 0); }); // #3511: a cross-phase, stray, or ad-hoc UAT/VERIFICATION file sitting in // this phase's directory must not surface under this phase's audit-uat // entry; this phase's own UAT/VERIFICATION artifacts must keep reporting // exactly as before (non-stray case unchanged). test('#3511: cross-phase stray UAT/VERIFICATION files in the same dir do not surface; own artifacts still do', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-foo'); fs.mkdirSync(phaseDir, { recursive: true }); // This phase's own UAT — must still report its pending item. fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), [ '---', 'status: partial', '---', '', '## Tests', '', '### 1. Own Test', 'expected: Works', 'result: pending', '', ].join('\n')); // This phase's own VERIFICATION — must still report its human-needed item. fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), [ '---', 'status: human_needed', 'phase: 03-foo', '---', '', '## Human Verification', '', '1. Own human check', ].join('\n')); // Cross-phase strays sitting in the SAME directory — token "04", not "03". fs.writeFileSync(path.join(phaseDir, '04-UAT.md'), [ '---', 'status: partial', '---', '', '## Tests', '', '### 1. Stray Test', 'expected: Works', 'result: pending', '', ].join('\n')); fs.writeFileSync(path.join(phaseDir, '04-VERIFICATION.md'), [ '---', 'status: human_needed', 'phase: 04-bar', '---', '', '## Human Verification', '', '1. Stray human check', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_files, 2, `only this phase's own 2 files must be scanned; got: ${JSON.stringify(output.results.map(r => r.file))}`); assert.strictEqual(output.summary.total_items, 2, `1 own UAT item + 1 own VERIFICATION item, strays excluded; got: ${output.summary.total_items}`); assert.strictEqual(output.summary.by_phase['03'], 2, 'own phase must be credited both items'); assert.ok(!('04' in output.summary.by_phase), 'the cross-phase stray must not appear in by_phase at all'); assert.ok(!result.output.includes('04-UAT.md'), 'stray UAT filename must never surface in the output'); assert.ok(!result.output.includes('04-VERIFICATION.md'), 'stray VERIFICATION filename must never surface in the output'); assert.ok(output.results.some(r => r.file === '03-UAT.md' && r.items.some(i => i.name === 'Own Test'))); assert.ok(output.results.some(r => r.file === '03-VERIFICATION.md' && r.items.some(i => i.name === 'Own human check'))); }); // #3511 follow-up: over-exclusion check on the #2528 digit-leading-slug // family. "05-80-20-cleanup" tokenizes to "05-80-20" (mis-absorbed past // the digit run scaffold actually writes into), so a literal token compare // excluded the phase's own report — audit-uat reported total_files: 0. test('#3511 follow-up: own UAT file still surfaces from the digit-leading-slug dir "05-80-20-cleanup" (over-exclusion check)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '05-80-20-cleanup'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '05-UAT.md'), [ '---', 'status: partial', '---', '', '## Tests', '', '### 1. Own Test', 'expected: Works', 'result: pending', '', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_files, 1, `own UAT file in a digit-leading-slug dir must still surface; got: ${JSON.stringify(output)}`); assert.strictEqual(output.summary.by_phase['05'], 1); }); // Regression: #2286 — parseUatItems never scanned a `## Gaps` section, so a // *-UAT.md file recording its only outstanding findings there returned // total_items: 0 (false-clean). Boundary: 0 / 1 / 2+ unresolved entries. describe('Gaps separator lines are not items (#3898)', () => { // The reporter's exact measurement table: every separator shape must // yield ONLY the real entry. A spaced hyphen break matched the item // opener regex (/^(\s*)-\s/) and fabricated a gap named '- -' with // result 'unknown' — unfixable by editing any entry, because there is // no entry, only the separator the author put there deliberately. const mkDoc = (sep) => [ '---', 'status: partial', 'phase: 01-x', '---', '', '## Gaps', '', sep, '- truth: real', ' status: open', '', ].join('\n'); const SEPARATORS = [ '- - -', '- -', '- - -', '- - - -', ' - - -', // unaffected forms stay unaffected (accidentally today, by handling after the fix) '---', '----', '* * *', '___', ]; for (const sep of SEPARATORS) { test(`separator ${JSON.stringify(sep)} yields only the real entry`, () => { const items = parseUatItems(mkDoc(sep)); assert.deepStrictEqual( items.map((i) => i.name), ['real'], `a thematic break must be a separator, not an entry (#3898); got ${JSON.stringify(items.map((i) => i.name))}`, ); }); } test('property: any bullet line whose remainder is only hyphens/spaces (>=2 hyphens) yields no item', () => { // CLAUDE.md's parser-contract convention: table coverage above, property // coverage here — arbitrary spacings and counts, not just the table's nine. fc.assert(fc.property( fc.integer({ min: 2, max: 6 }), // extra hyphens fc.integer({ min: 0, max: 3 }), // leading indent fc.integer({ min: 1, max: 3 }), // spaces between hyphens (hyphens, indent, gap) => { const pad = ' '.repeat(indent); const sep = pad + Array(hyphens + 1).fill('-').join(' '.repeat(gap)); const items = parseUatItems(mkDoc(sep)); return items.length === 1 && items[0].name === 'real'; }, ), { seed: 20260829, numRuns: 60 }); }); test('#3898 review: a separator inside a live entry keeps its span contiguous (ack-able)', () => { // Disposition (a): a separator deeper than baseIndent folds back as a // continuation line, so entry lines and the entry's byte span agree — // the ack writer's identity re-verification still matches. const items = parseUatItems([ '---', 'status: partial', 'phase: 01-x', '---', '', '## Gaps', '', '- truth: real', ' - - -', ' status: open', '', ].join('\n')); assert.deepStrictEqual(items.map((i) => i.name), ['real']); }); test('a real entry whose text starts with a hyphen is still an entry (no over-skip)', () => { const items = parseUatItems([ '---', 'status: partial', 'phase: 01-x', '---', '', '## Gaps', '', '- truth: "-5 error budget remaining"', ' status: open', '', ].join('\n')); assert.deepStrictEqual(items.map((i) => i.name), ['-5 error budget remaining']); }); }); describe('Gaps section scanning (#2286)', () => { test('a Gaps-only UAT file with 0 unresolved entries (all resolved) yields no items', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: partial', 'phase: 01-foundation', '---', '', '## Gaps', '', '', '- truth: "SC1: Widget renders with data"', ' status: resolved', ' reason: "Fixed in follow-up commit"', '', '- truth: "SC2: Second finding also fixed"', ' status: resolved', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 0, 'resolved Gaps entries must not be counted as outstanding items'); assert.strictEqual(output.summary.total_files, 0); }); test('a Gaps-only UAT file with exactly 1 unresolved entry and zero ### N. test blocks yields 1 item', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: partial', 'phase: 01-foundation', '---', '', '## Gaps', '', '', '- truth: "SC1: Widget renders with data"', ' status: open', ' reason: "Missing data binding"', ' severity: major', ' test: 2', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1, 'total_items must be > 0, not the false-clean 0'); assert.strictEqual(output.results[0].type, 'uat'); assert.strictEqual(output.results[0].items[0].name, 'SC1: Widget renders with data'); assert.strictEqual(output.results[0].items[0].result, 'open'); assert.strictEqual(output.results[0].items[0].reason, 'Missing data binding'); assert.strictEqual(output.results[0].items[0].test, 2); }); test('a Gaps section with 2+ unresolved entries surfaces all of them and skips the resolved one', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '02-api'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '02-UAT.md'), [ '---', 'status: partial', 'phase: 02-api', '---', '', '## Gaps', '', '', '- truth: "SC1: First outstanding gap"', ' status: failed', ' reason: "Endpoint returns 500"', '', '- truth: "SC2: Second outstanding gap"', ' status: open', '', '- truth: "SC3: Already fixed gap"', ' status: resolved', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 2, 'exactly the 2 unresolved gaps should be counted, resolved gap excluded'); const names = output.results[0].items.map((item) => item.name).sort(); assert.deepStrictEqual(names, ['SC1: First outstanding gap', 'SC2: Second outstanding gap']); }); // Regression: #2286 review HIGH finding — a naive whole-string `key:` // scan over a Gaps entry's flattened text matches the FIRST `key:`-shaped // substring anywhere, including one embedded inside an EARLIER field's // own quoted free-text value. A `truth`/`reason` value that itself // contains the literal text "status: resolved" (or "reason:"/"test:") // must never hijack the real, later `status:`/`reason:`/`test:` field — // the fix parses each field anchored to the START of its own line. test('a truth value containing the literal substring "status: resolved" does not suppress the real open status', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: partial', 'phase: 01-foundation', '---', '', '## Gaps', '', '', '- truth: "The status: resolved workflow should trigger a banner"', ' status: failed', ' reason: "Contains a reason: field embedded phrase, and test: 9 too"', ' test: 3', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1, 'the genuinely open gap must be surfaced, not dropped because its truth text contains "status: resolved"'); const item = output.results[0].items[0]; assert.strictEqual(item.name, 'The status: resolved workflow should trigger a banner'); assert.strictEqual(item.result, 'failed', 'the REAL status: field must win, not the embedded phrase inside truth'); assert.strictEqual(item.reason, 'Contains a reason: field embedded phrase, and test: 9 too', 'the reason value is taken verbatim, including its own embedded colon-bearing phrases'); assert.strictEqual(item.test, 3, 'the REAL test: field (3) must win, not the "test: 9" phrase embedded in reason'); }); // Regression: #2286 review LOW finding — a nested `artifacts:` sub-list // (per templates/UAT.md's `## Gaps` schema) must be folded into its // parent entry, not mis-split into spurious standalone items. test('a Gaps entry with a nested artifacts sub-list parses as exactly one item', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: partial', 'phase: 01-foundation', '---', '', '## Gaps', '', '', '- truth: "SC1: Some behavior"', ' status: failed', ' reason: "reason text"', ' severity: major', ' test: 1', ' root_cause: ""', ' artifacts:', ' - src/foo.ts', ' - src/bar.ts', ' missing: []', ' debug_session: ""', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1, 'the nested artifacts sub-list items must not spawn spurious extra Gaps items'); assert.strictEqual(output.results[0].items[0].name, 'SC1: Some behavior'); assert.strictEqual(output.results[0].items[0].category, 'unknown', 'a Gaps item with no dedicated category mapping falls back to unknown'); }); // Regression: #2286 review item 5 (fail-safe direction) — #2286 is a // false-NEGATIVE bug, so a Gaps entry with no parseable `status:` field // is surfaced (as result: 'unknown') rather than silently dropped. test('a Gaps entry with no status field is surfaced as an unknown-status item (fail-safe)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: partial', 'phase: 01-foundation', '---', '', '## Gaps', '', '- truth: "SC1: Missing status field entirely"', ' reason: "why it is open"', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 1, 'a garbled/missing status must SURFACE the entry, not silently drop it'); assert.strictEqual(output.results[0].items[0].result, 'unknown'); assert.strictEqual(output.results[0].items[0].name, 'SC1: Missing status field entirely'); }); test('an empty Gaps section (heading present, no bullets) yields 0 items without throwing', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [ '---', 'status: partial', 'phase: 01-foundation', '---', '', '## Gaps', '', ].join('\n')); const result = runMsdTools('audit-uat --raw', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.summary.total_items, 0); assert.strictEqual(output.summary.total_files, 0); }); // ─── #3879 review round 4, Major: `status:` is authoritative ────────────── // // A `resolution:` note beside a status that is not `resolved` is an // authoring mistake, not a closure assertion — the rule `validateResolution` // (probe-core.cts) already applies to this same field pair, where it rejects // the combination outright rather than counting the item as closed. A // reporter cannot throw, so the fail-safe equivalent is to SURFACE the item. // Dropping it would be the silently-vanishing-item defect #3850 exists to // close, reached by field COMBINATION instead of file STATUS. test('a human_verification entry whose status contradicts its resolution still surfaces', () => { const items = parseVerificationItems(`--- status: human_needed human_verification: - test: "Retry the upload" status: failed resolution: "attempted retry, still failing" --- `, 'human_needed'); assert.strictEqual(items.length, 1); }); test('a gaps entry carrying only a resolution note surfaces — gaps closes on status alone', () => { // `parseGapsItems`, the `## Gaps` markdown reader, closes on // `status: resolved` and nothing else. This frontmatter reader takes that // rule verbatim so the same authored entry cannot read closed in one and // open in the other. const items = parseVerificationItems(`--- status: gaps_found gaps: - truth: "The widget renders" resolution: "a note, not a closure assertion" --- `, 'gaps_found'); assert.strictEqual(items.length, 1); assert.strictEqual(items[0].result, 'unknown'); }); test('a gaps entry whose status contradicts its resolution still surfaces', () => { const items = parseVerificationItems(`--- status: gaps_found gaps: - truth: "The widget renders" status: failed resolution: "attempted retry, still failing" --- `, 'gaps_found'); assert.strictEqual(items.length, 1); assert.strictEqual(items[0].result, 'failed'); }); // ─── #3879 review round 4, Minor 2: the alignment guard ────────────────── // // `parsedEntriesFor` pairs the display array with the parsed array BY INDEX // and degrades to all-null if their lengths disagree, so a mis-paired index // can never close the wrong row. That branch is unreachable through the two // readers — both parsers share `frontmatterRegion`, and the display step is // 1:1 — so it is asserted against the function directly. Called with the // real content the readers pass, plus a `flattened` array of the wrong // length, which is exactly the drift the guard exists to catch. describe('parsedEntriesFor: index pairing and its degradation', () => { const doc = `--- status: gaps_found gaps: - truth: "first" status: failed - truth: "second" status: resolved --- `; test('pairs each display entry with its own parsed object', () => { const paired = parsedEntriesFor(doc, 'gaps', ['first-display', 'second-display']); assert.equal(paired.length, 2); assert.equal(paired[0].truth, 'first'); assert.equal(paired[1].truth, 'second'); }); test('a length disagreement degrades to all-null — no entry is skipped as closed', () => { // Over-reporting is the correct degradation: a wrong index would name a // DIFFERENT entry's fields and close the wrong row. const shorter = parsedEntriesFor(doc, 'gaps', ['only-one']); assert.deepEqual(shorter, [null]); const longer = parsedEntriesFor(doc, 'gaps', ['a', 'b', 'c']); assert.deepEqual(longer, [null, null, null]); }); test('a non-object entry is null at its own index, not dropped', () => { const mixed = `--- gaps: - truth: "an object" - a bare scalar --- `; const paired = parsedEntriesFor(mixed, 'gaps', ['x', 'y']); assert.equal(paired.length, 2); assert.equal(paired[0].truth, 'an object'); assert.equal(paired[1], null); }); test('an absent key degrades to all-null rather than throwing', () => { assert.deepEqual(parsedEntriesFor('---\nother: 1\n---\n', 'gaps', ['a', 'b']), [null, null]); }); }); test('both closure spellings still close a human_verification entry', () => { const items = parseVerificationItems(`--- status: human_needed human_verification: - test: "Answered" resolution: "RESOLVED" - test: "Also answered" status: resolved - test: "Still open" status: partial --- `, 'human_needed'); assert.strictEqual(items.length, 1); assert.strictEqual(items[0].test, 3, 'the surfaced entry keeps its original row'); }); }); // Regression: #2286 — parseVerificationItems never read the frontmatter's // structured `human_verification:` YAML array, and never recognized the // `### N.