From cee8d7d7235c020640313cef6eebaa92e69d10d1 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 12 Jul 2026 13:32:59 -0400 Subject: [PATCH] test(#2140): cover checkbox/table divergence in requirements mark-complete Adds the fixture the green suite lacked: an ID with a checkbox and a traceability table that does NOT mention it. Asserts (1) a checkbox-only reconcile surfaces table_unmatched instead of a silent full-success payload, (2) re-run on the half-written file does NOT mask the drift as already_complete, and (3) a REQUIREMENTS.md with no traceability table stays a clean success. --- tests/milestone.test.cjs | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index dd65626b5..915c7a3d1 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -488,6 +488,71 @@ describe('requirements mark-complete command', () => { | INFRA-01 | Phase 6 | Pending | `; + // #2140: a traceability table EXISTS but has no row for the ID being completed. + // Only the checkbox can reconcile; the table surface is unsynced. The CLI must + // not report this as a payload indistinguishable from a full reconcile. + const TABLE_WITHOUT_FOO = `# Requirements + +## Coverage +- [ ] **FOO-01**: feature one +- [ ] **BAR-01**: feature two + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| BAR-01 | Phase 1 | Pending | +`; + + test('#2140 checkbox-only reconcile surfaces table_unmatched (not silent success)', () => { + writeRequirements(tmpDir, TABLE_WITHOUT_FOO); + + const result = runGsdTools('requirements mark-complete FOO-01', tmpDir); + assert.ok(result.success); + + const out = JSON.parse(result.output); + // The checkbox WAS written, so it is in marked_complete... + assert.ok(out.marked_complete.includes('FOO-01'), 'checkbox reconcile is reported'); + // ...but the traceability table had no FOO-01 row, which must be surfaced. + assert.ok(Array.isArray(out.table_unmatched), 'table_unmatched bucket must exist'); + assert.ok(out.table_unmatched.includes('FOO-01'), + 'an ID with a checkbox but no table row must be surfaced as table_unmatched'); + + const content = readRequirements(tmpDir); + assert.ok(content.includes('- [x] **FOO-01**'), 'checkbox should be checked'); + // The table is untouched (no FOO-01 row synthesized). + assert.ok(!content.includes('FOO-01 | Phase'), 'no FOO-01 row should be invented'); + }); + + test('#2140 re-run on the half-written file does NOT mask the drift as already_complete', () => { + writeRequirements(tmpDir, TABLE_WITHOUT_FOO); + // First run: flips the checkbox, surfaces table_unmatched. + runGsdTools('requirements mark-complete FOO-01', tmpDir); + // Second run on the now-[x]-checkbox-with-no-row file. + const result = runGsdTools('requirements mark-complete FOO-01', tmpDir); + assert.ok(result.success); + const out = JSON.parse(result.output); + + assert.ok(!out.already_complete.includes('FOO-01'), + 'a [x] checkbox with no table row is PARTIALLY reconciled, not already_complete'); + assert.ok(!out.marked_complete.includes('FOO-01'), + 'nothing flipped on re-run, so not marked_complete'); + assert.ok(out.table_unmatched.includes('FOO-01'), + 'the drift must still be surfaced as table_unmatched on re-run'); + }); + + test('#2140 no traceability table at all → still a clean success (no table_unmatched)', () => { + // A REQUIREMENTS.md with no traceability table is legitimate; a checkbox-only + // reconcile must remain an unqualified success with no table_unmatched entry. + writeRequirements(tmpDir, '# Requirements\n\n- [ ] **NO-TABLE-01**: thing\n'); + const result = runGsdTools('requirements mark-complete NO-TABLE-01', tmpDir); + assert.ok(result.success); + const out = JSON.parse(result.output); + assert.ok(out.marked_complete.includes('NO-TABLE-01')); + assert.ok(!out.table_unmatched || !out.table_unmatched.includes('NO-TABLE-01'), + 'no table_unmatched when there is no traceability table'); + }); + test('marks single requirement complete (checkbox + table)', () => { writeRequirements(tmpDir, STANDARD_REQUIREMENTS);