|
| 1 | +# SPDX-License-Identifier: MIT |
| 2 | +# Copyright (c) 2026 BitConcepts, LLC. All rights reserved. |
| 3 | +"""Tests for REQ-358 (_req_count H2 support) and REQ-359 (sync YAML-mode MD fallback).""" |
| 4 | + |
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +import json |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | + |
| 11 | +# ── REQ-359 / TEST-359: _req_count H2 heading support ───────────────────────── |
| 12 | + |
| 13 | + |
| 14 | +class TestReqCountH2: |
| 15 | + """_req_count should match ## REQ-XX-NNN (H2) headings, not just ### (H3).""" |
| 16 | + |
| 17 | + def test_req_count_h2_headings(self, tmp_path: Path) -> None: |
| 18 | + from specsmith.phase import _req_count |
| 19 | + |
| 20 | + docs = tmp_path / "docs" |
| 21 | + docs.mkdir() |
| 22 | + lines = [f"## REQ-BE-{i:03d}: Requirement {i}" for i in range(1, 6)] |
| 23 | + (docs / "REQUIREMENTS.md").write_text("\n".join(lines) + "\n", encoding="utf-8") |
| 24 | + |
| 25 | + assert _req_count(5)(tmp_path) is True |
| 26 | + assert _req_count(6)(tmp_path) is False # strict boundary |
| 27 | + |
| 28 | + def test_req_count_h3_still_works(self, tmp_path: Path) -> None: |
| 29 | + from specsmith.phase import _req_count |
| 30 | + |
| 31 | + docs = tmp_path / "docs" |
| 32 | + docs.mkdir() |
| 33 | + lines = [f"### REQ-{i:03d}: Requirement {i}" for i in range(1, 4)] |
| 34 | + (docs / "REQUIREMENTS.md").write_text("\n".join(lines) + "\n", encoding="utf-8") |
| 35 | + |
| 36 | + assert _req_count(3)(tmp_path) is True |
| 37 | + assert _req_count(4)(tmp_path) is False |
| 38 | + |
| 39 | + def test_req_count_mixed_h2_h3(self, tmp_path: Path) -> None: |
| 40 | + from specsmith.phase import _req_count |
| 41 | + |
| 42 | + docs = tmp_path / "docs" |
| 43 | + docs.mkdir() |
| 44 | + content = ( |
| 45 | + "## REQ-BE-001: First\n" |
| 46 | + "### REQ-BE-002: Second\n" |
| 47 | + "## REQ-BE-003: Third\n" |
| 48 | + ) |
| 49 | + (docs / "REQUIREMENTS.md").write_text(content, encoding="utf-8") |
| 50 | + |
| 51 | + assert _req_count(3)(tmp_path) is True |
| 52 | + assert _req_count(4)(tmp_path) is False |
| 53 | + |
| 54 | + |
| 55 | +# ── REQ-358 / TEST-358: sync YAML-mode Markdown fallback ────────────────────── |
| 56 | + |
| 57 | + |
| 58 | +class TestSyncYamlModeMarkdownFallback: |
| 59 | + """run_sync in YAML mode should fall back to REQUIREMENTS.md parsing |
| 60 | + when no YAML requirement files exist but REQUIREMENTS.md has content.""" |
| 61 | + |
| 62 | + def test_sync_yaml_mode_markdown_fallback(self, tmp_path: Path) -> None: |
| 63 | + from specsmith.sync import run_sync |
| 64 | + |
| 65 | + # Set up YAML mode |
| 66 | + state_dir = tmp_path / ".specsmith" |
| 67 | + state_dir.mkdir() |
| 68 | + (state_dir / "governance-mode").write_text("yaml", encoding="utf-8") |
| 69 | + |
| 70 | + # Create REQUIREMENTS.md with H2 headings (no YAML files) |
| 71 | + docs = tmp_path / "docs" |
| 72 | + docs.mkdir() |
| 73 | + lines = [f"## REQ-BE-{i:03d}: Backend requirement {i}" for i in range(1, 7)] |
| 74 | + (docs / "REQUIREMENTS.md").write_text("\n".join(lines) + "\n", encoding="utf-8") |
| 75 | + |
| 76 | + # No docs/requirements/ directory — forces fallback |
| 77 | + |
| 78 | + result = run_sync(tmp_path) |
| 79 | + |
| 80 | + assert result.reqs_after >= 6 |
| 81 | + reqs_json = state_dir / "requirements.json" |
| 82 | + assert reqs_json.exists() |
| 83 | + data = json.loads(reqs_json.read_text(encoding="utf-8")) |
| 84 | + assert len(data) == 6 |
| 85 | + |
| 86 | + def test_sync_yaml_mode_no_fallback_when_yaml_exists(self, tmp_path: Path) -> None: |
| 87 | + """When YAML files exist, no fallback should occur.""" |
| 88 | + from specsmith.sync import run_sync |
| 89 | + |
| 90 | + state_dir = tmp_path / ".specsmith" |
| 91 | + state_dir.mkdir() |
| 92 | + (state_dir / "governance-mode").write_text("yaml", encoding="utf-8") |
| 93 | + |
| 94 | + docs = tmp_path / "docs" |
| 95 | + docs.mkdir() |
| 96 | + req_dir = docs / "requirements" |
| 97 | + req_dir.mkdir() |
| 98 | + (req_dir / "core.yml").write_text( |
| 99 | + "- id: REQ-001\n title: First\n status: defined\n", |
| 100 | + encoding="utf-8", |
| 101 | + ) |
| 102 | + |
| 103 | + result = run_sync(tmp_path) |
| 104 | + |
| 105 | + # Should use YAML source, not fallback |
| 106 | + assert result.reqs_after == 1 |
0 commit comments