diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index cff09ec33..43f2b563e 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -221,6 +221,166 @@ describe('milestone complete command', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// requirements mark-complete command +// ───────────────────────────────────────────────────────────────────────────── + +describe('requirements mark-complete command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + // ─── helpers ────────────────────────────────────────────────────────────── + + function writeRequirements(tmpDir, content) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), content, 'utf-8'); + } + + function readRequirements(tmpDir) { + return fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); + } + + const STANDARD_REQUIREMENTS = `# Requirements + +## Test Coverage +- [ ] **TEST-01**: core.cjs has tests for loadConfig +- [ ] **TEST-02**: core.cjs has tests for resolveModelInternal +- [x] **TEST-03**: core.cjs has tests for escapeRegex (already complete) + +## Bug Regressions +- [ ] **REG-01**: Test confirms loadConfig returns model_overrides + +## Infrastructure +- [ ] **INFRA-01**: GitHub Actions workflow runs tests + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| TEST-01 | Phase 1 | Pending | +| TEST-02 | Phase 1 | Pending | +| TEST-03 | Phase 1 | Complete | +| REG-01 | Phase 1 | Pending | +| INFRA-01 | Phase 6 | Pending | +`; + + // ─── tests ──────────────────────────────────────────────────────────────── + + test('marks single requirement complete (checkbox + table)', () => { + writeRequirements(tmpDir, STANDARD_REQUIREMENTS); + + const result = runGsdTools('requirements mark-complete TEST-01', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.updated, true); + assert.ok(output.marked_complete.includes('TEST-01'), 'TEST-01 should be marked complete'); + + const content = readRequirements(tmpDir); + assert.ok(content.includes('- [x] **TEST-01**'), 'checkbox should be checked'); + assert.ok(content.includes('| TEST-01 | Phase 1 | Complete |'), 'table row should be Complete'); + // Other checkboxes unchanged + assert.ok(content.includes('- [ ] **TEST-02**'), 'TEST-02 should remain unchecked'); + }); + + test('handles mixed prefixes in single call (TEST-XX, REG-XX, INFRA-XX)', () => { + writeRequirements(tmpDir, STANDARD_REQUIREMENTS); + + const result = runGsdTools('requirements mark-complete TEST-01,REG-01,INFRA-01', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.marked_complete.length, 3, 'should mark 3 requirements complete'); + assert.ok(output.marked_complete.includes('TEST-01')); + assert.ok(output.marked_complete.includes('REG-01')); + assert.ok(output.marked_complete.includes('INFRA-01')); + + const content = readRequirements(tmpDir); + assert.ok(content.includes('- [x] **TEST-01**'), 'TEST-01 checkbox should be checked'); + assert.ok(content.includes('- [x] **REG-01**'), 'REG-01 checkbox should be checked'); + assert.ok(content.includes('- [x] **INFRA-01**'), 'INFRA-01 checkbox should be checked'); + assert.ok(content.includes('| TEST-01 | Phase 1 | Complete |'), 'TEST-01 table should be Complete'); + assert.ok(content.includes('| REG-01 | Phase 1 | Complete |'), 'REG-01 table should be Complete'); + assert.ok(content.includes('| INFRA-01 | Phase 6 | Complete |'), 'INFRA-01 table should be Complete'); + }); + + test('accepts space-separated IDs', () => { + writeRequirements(tmpDir, STANDARD_REQUIREMENTS); + + const result = runGsdTools('requirements mark-complete TEST-01 TEST-02', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.marked_complete.length, 2, 'should mark 2 requirements complete'); + + const content = readRequirements(tmpDir); + assert.ok(content.includes('- [x] **TEST-01**'), 'TEST-01 should be checked'); + assert.ok(content.includes('- [x] **TEST-02**'), 'TEST-02 should be checked'); + }); + + test('accepts bracket-wrapped IDs [REQ-01, REQ-02]', () => { + writeRequirements(tmpDir, STANDARD_REQUIREMENTS); + + const result = runGsdTools('requirements mark-complete [TEST-01,TEST-02]', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.marked_complete.length, 2, 'should mark 2 requirements complete'); + + const content = readRequirements(tmpDir); + assert.ok(content.includes('- [x] **TEST-01**'), 'TEST-01 should be checked'); + assert.ok(content.includes('- [x] **TEST-02**'), 'TEST-02 should be checked'); + }); + + test('returns not_found for invalid IDs while updating valid ones', () => { + writeRequirements(tmpDir, STANDARD_REQUIREMENTS); + + const result = runGsdTools('requirements mark-complete TEST-01,FAKE-99', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.updated, true, 'should still update valid IDs'); + assert.ok(output.marked_complete.includes('TEST-01'), 'TEST-01 should be marked complete'); + assert.ok(output.not_found.includes('FAKE-99'), 'FAKE-99 should be in not_found'); + assert.strictEqual(output.total, 2, 'total should reflect all IDs attempted'); + }); + + test('idempotent — re-marking already-complete requirement does not corrupt', () => { + writeRequirements(tmpDir, STANDARD_REQUIREMENTS); + + // TEST-03 already has [x] and Complete in the fixture + const result = runGsdTools('requirements mark-complete TEST-03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Regex only matches [ ] (space), not [x], so TEST-03 goes to not_found + assert.ok(output.not_found.includes('TEST-03'), 'already-complete ID should be in not_found'); + + const content = readRequirements(tmpDir); + // File should not be corrupted — no [xx] or doubled markers + assert.ok(content.includes('- [x] **TEST-03**'), 'existing [x] should remain intact'); + assert.ok(!content.includes('[xx]'), 'should not have doubled x markers'); + assert.ok(!content.includes('- [x] [x]'), 'should not have duplicate checkbox'); + }); + + test('missing REQUIREMENTS.md returns expected error structure', () => { + // createTempProject does not create REQUIREMENTS.md — so it's already missing + + const result = runGsdTools('requirements mark-complete TEST-01', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.updated, false, 'updated should be false'); + assert.strictEqual(output.reason, 'REQUIREMENTS.md not found', 'should report file not found'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // validate consistency command // ─────────────────────────────────────────────────────────────────────────────