test_lt_unit.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
| 1 | """Unit tests for LT loop tightening (§LT.10).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | |
| 7 | import pytest |
| 8 | |
| 9 | from adapters.config import load_config |
| 10 | from adapters.errors import ConfigError |
| 11 | from cli.version_lock import ORIGIN_KIT, FootprintEntry, build_version_lock_from_entries |
| 12 | from tools.footprint_coverage import check_footprint_coverage |
| 13 | from tools.handover_compact import compact_handover_change_log |
| 14 | from tools.optional_feature_tips import build_optional_feature_tips |
| 15 | from tools.verification_evidence_gate import build_verification_evidence_gate |
| 16 | from tests.support import FIXTURES, KIT_ROOT, load_fixture_config, write_config |
| 17 | |
| 18 | |
| 19 | def _lock(entries: list[FootprintEntry]): |
| 20 | return build_version_lock_from_entries( |
| 21 | kit_version="0.1.0", |
| 22 | config_version=1, |
| 23 | entries=entries, |
| 24 | installed_at="2026-01-01T00:00:00Z", |
| 25 | ) |
| 26 | |
| 27 | |
| 28 | def test_coverage_empty_lock_not_applicable(tmp_path: Path) -> None: |
| 29 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 30 | report = check_footprint_coverage(tmp_path, config, lock=_lock([]), rendered=[]) |
| 31 | assert report.ok |
| 32 | assert report.state == "not_applicable" |
| 33 | |
| 34 | |
| 35 | def test_coverage_missing_from_lock(tmp_path: Path) -> None: |
| 36 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 37 | from cli.footprint import resolve_footprint |
| 38 | |
| 39 | files = resolve_footprint(config, kit=KIT_ROOT) |
| 40 | lock = _lock( |
| 41 | [ |
| 42 | FootprintEntry( |
| 43 | path=files[0].destination, |
| 44 | source=files[0].source, |
| 45 | sha256="0" * 64, |
| 46 | origin=ORIGIN_KIT, |
| 47 | ) |
| 48 | ] |
| 49 | ) |
| 50 | report = check_footprint_coverage(tmp_path, config, lock=lock, rendered=files, kit=KIT_ROOT) |
| 51 | assert not report.ok |
| 52 | assert report.state == "missing_from_lock" |
| 53 | assert report.remediation == "ok sync" |
| 54 | assert len(report.missing) == len(files) - 1 |
| 55 | |
| 56 | |
| 57 | def test_coverage_all_dests_ok(tmp_path: Path) -> None: |
| 58 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 59 | from cli.footprint import resolve_footprint |
| 60 | |
| 61 | files = resolve_footprint(config, kit=KIT_ROOT) |
| 62 | entries = [ |
| 63 | FootprintEntry(path=f.destination, source=f.source, sha256="0" * 64, origin=ORIGIN_KIT) |
| 64 | for f in files |
| 65 | ] |
| 66 | report = check_footprint_coverage( |
| 67 | tmp_path, config, lock=_lock(entries), rendered=files, kit=KIT_ROOT |
| 68 | ) |
| 69 | assert report.ok |
| 70 | assert report.state == "ok" |
| 71 | |
| 72 | |
| 73 | def test_session_bookends_default_off(tmp_path: Path) -> None: |
| 74 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 75 | assert config.session_bookends.enabled is False |
| 76 | |
| 77 | |
| 78 | def test_session_bookends_unknown_key(tmp_path: Path) -> None: |
| 79 | dest = tmp_path / ".overseer" / "config.yaml" |
| 80 | dest.parent.mkdir(parents=True, exist_ok=True) |
| 81 | text = (FIXTURES / "config-git-only.yaml").read_text(encoding="utf-8") |
| 82 | dest.write_text(text + "\nsession_bookends:\n enabled: false\n extra: true\n", encoding="utf-8") |
| 83 | with pytest.raises(ConfigError, match="unknown session_bookends"): |
| 84 | load_config(dest) |
| 85 | |
| 86 | |
| 87 | def test_session_bookends_enabled_non_bool(tmp_path: Path) -> None: |
| 88 | dest = tmp_path / ".overseer" / "config.yaml" |
| 89 | dest.parent.mkdir(parents=True, exist_ok=True) |
| 90 | text = (FIXTURES / "config-git-only.yaml").read_text(encoding="utf-8") |
| 91 | dest.write_text(text + "\nsession_bookends:\n enabled: not-a-bool\n", encoding="utf-8") |
| 92 | with pytest.raises(ConfigError, match="must be a boolean"): |
| 93 | load_config(dest) |
| 94 | |
| 95 | |
| 96 | def test_compact_keep_too_low(tmp_path: Path) -> None: |
| 97 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 98 | write_config(tmp_path, "config-git-only.yaml") |
| 99 | handover = tmp_path / "docs" / "OVERSEER-HANDOVER.md" |
| 100 | handover.parent.mkdir(parents=True, exist_ok=True) |
| 101 | handover.write_text( |
| 102 | "<!-- overseer:anchor:change-log -->\n" |
| 103 | "- **2026-01-01** — one\n" |
| 104 | "<!-- /overseer:anchor:change-log -->\n", |
| 105 | encoding="utf-8", |
| 106 | ) |
| 107 | from cli.main import main |
| 108 | from cli.context import CliContext |
| 109 | from cli.output import OutputContext |
| 110 | from tests.support import make_runner |
| 111 | |
| 112 | ctx = CliContext.create( |
| 113 | runner=make_runner({}), |
| 114 | cwd=tmp_path, |
| 115 | kit=KIT_ROOT, |
| 116 | output=OutputContext(json_mode=False), |
| 117 | ) |
| 118 | import os |
| 119 | |
| 120 | old = os.getcwd() |
| 121 | os.chdir(tmp_path) |
| 122 | try: |
| 123 | code = main(["handover-compact", "--keep", "4"], ctx=ctx) |
| 124 | finally: |
| 125 | os.chdir(old) |
| 126 | assert code == 2 |
| 127 | |
| 128 | |
| 129 | def test_compact_missing_anchor(tmp_path: Path) -> None: |
| 130 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 131 | handover = tmp_path / "docs" / "OVERSEER-HANDOVER.md" |
| 132 | handover.parent.mkdir(parents=True, exist_ok=True) |
| 133 | handover.write_text("# no anchor\n", encoding="utf-8") |
| 134 | report = compact_handover_change_log(config, tmp_path, keep=15, write=False) |
| 135 | assert not report.ok |
| 136 | assert report.reason == "change_log_anchor_missing" |
| 137 | |
| 138 | |
| 139 | def test_compact_no_op_when_within_keep(tmp_path: Path) -> None: |
| 140 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 141 | handover = tmp_path / "docs" / "OVERSEER-HANDOVER.md" |
| 142 | handover.parent.mkdir(parents=True, exist_ok=True) |
| 143 | handover.write_text( |
| 144 | "<!-- overseer:anchor:change-log -->\n" |
| 145 | "- **2026-01-01** — one\n" |
| 146 | "- **2026-01-02** — two\n" |
| 147 | "<!-- /overseer:anchor:change-log -->\n", |
| 148 | encoding="utf-8", |
| 149 | ) |
| 150 | report = compact_handover_change_log(config, tmp_path, keep=15, write=False) |
| 151 | assert report.ok |
| 152 | assert report.compacted == 0 |
| 153 | |
| 154 | |
| 155 | def test_verification_gate_skips_when_honesty_off(tmp_path: Path) -> None: |
| 156 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 157 | report = build_verification_evidence_gate(config, tmp_path) |
| 158 | assert report.skipped |
| 159 | assert report.ok |
| 160 | |
| 161 | |
| 162 | def test_verification_gate_skips_active_auto_todo(tmp_path: Path) -> None: |
| 163 | write_config(tmp_path, "config-git-only.yaml") |
| 164 | config_path = tmp_path / ".overseer" / "config.yaml" |
| 165 | text = config_path.read_text(encoding="utf-8") |
| 166 | config_path.write_text( |
| 167 | text |
| 168 | + "\nhonesty:\n enabled: true\n ledger: .overseer/honesty/VERDICT-LEDGER.jsonl\n" |
| 169 | + " require_verification_evidence: warn\n", |
| 170 | encoding="utf-8", |
| 171 | ) |
| 172 | config = load_config(config_path) |
| 173 | docs = tmp_path / "docs" |
| 174 | docs.mkdir(parents=True, exist_ok=True) |
| 175 | (docs / "ROADMAP.md").write_text( |
| 176 | "| Phase | Model | Status | Deliverable |\n" |
| 177 | "| --- | --- | --- | --- |\n" |
| 178 | "| **LT-b Loop tightening build** | Auto | **TODO** | build |\n", |
| 179 | encoding="utf-8", |
| 180 | ) |
| 181 | (docs / "OVERSEER-HANDOVER.md").write_text( |
| 182 | "## NEXT SESSION — LT-b\n\n| | |\n| **ID** | **LT-b** |\n", |
| 183 | encoding="utf-8", |
| 184 | ) |
| 185 | report = build_verification_evidence_gate(config, tmp_path) |
| 186 | assert report.skipped |
| 187 | |
| 188 | |
| 189 | def test_optional_feature_tips_default_off(tmp_path: Path) -> None: |
| 190 | config = load_fixture_config(tmp_path, "config-git-only.yaml") |
| 191 | tips = build_optional_feature_tips(config) |
| 192 | assert len(tips) == 2 |
| 193 | assert any("session_bookends off" in tip for tip in tips) |
| 194 | assert any("honesty off" in tip for tip in tips) |
| 195 | |
| 196 | |
| 197 | def test_optional_feature_tips_empty_when_enabled(tmp_path: Path) -> None: |
| 198 | write_config(tmp_path, "config-git-only.yaml") |
| 199 | config_path = tmp_path / ".overseer" / "config.yaml" |
| 200 | text = config_path.read_text(encoding="utf-8") |
| 201 | config_path.write_text( |
| 202 | text |
| 203 | + "\nsession_bookends:\n enabled: true\n" |
| 204 | + "honesty:\n enabled: true\n ledger: .overseer/honesty/VERDICT-LEDGER.jsonl\n" |
| 205 | + " require_independent_second_reviewer: warn\n", |
| 206 | encoding="utf-8", |
| 207 | ) |
| 208 | config = load_config(config_path) |
| 209 | assert build_optional_feature_tips(config) == [] |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago