import json from types import SimpleNamespace import pytest from test_evidence_local_archive import write_unit from test_preprocess_cli import _runtime_config from typer.testing import CliRunner from tht.cli import app from tht.evidence.administration import ConsolidationError, browse, consolidate_from_config from tht.evidence.local_archive import LocalEvidenceArchive def test_browse_filters_working_files_without_index_or_dwh_and_reports_invalid_files(tmp_path): path = write_unit(tmp_path) archive = LocalEvidenceArchive(tmp_path) archive.consolidate(actor="Alice", activate=lambda _: None) path.write_text(path.read_text().replace("Use order ID and year.", "Use all three parts of the key.")) invalid = path.parent / "broken.md" invalid.write_text("not an Evidence document") result = browse(tmp_path, {"kind": "domain", "q": "three"}) assert result["total"] == 1 assert result["items"][0]["status"] == "modified" assert result["items"][0]["payload"]["rule"] == "Use all three parts of the key." assert result["errors"][0]["file"] == "curated/domain/broken.md" path.unlink() assert browse(tmp_path, {"status": "removed"})["total"] == 1 def test_public_consolidation_retries_saved_content_and_does_not_use_git(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command import tht.config as config_module path = write_unit(tmp_path) cfg = SimpleNamespace(evidence=SimpleNamespace(local_archive_root=tmp_path)) monkeypatch.setattr(config_module, "load_config", lambda _: cfg) calls = [] def stage(_, *, local_snapshot): calls.append(local_snapshot) if len(calls) == 1: raise RuntimeError("private endpoint failure") return SimpleNamespace(status="succeeded") monkeypatch.setattr(command, "run_from_config", stage) monkeypatch.setattr("subprocess.run", lambda *a, **kw: pytest.fail("Consolidation must not run Git")) with pytest.raises(ConsolidationError, match="Retry consolidation") as failure: consolidate_from_config(tmp_path / "config.yaml") assert failure.value.saved assert path.exists() assert LocalEvidenceArchive(tmp_path).active_snapshot() is None assert consolidate_from_config(tmp_path / "config.yaml").status == "succeeded" assert calls[0] == calls[1] def test_cli_failure_is_pristine_json_with_a_correctable_error(monkeypatch, tmp_path): config = _runtime_config(tmp_path) def fail(_): raise ConsolidationError("curated/domain/rule.md: Missing Rule section") monkeypatch.setattr("tht.evidence.administration.consolidate_from_config", fail) result = CliRunner().invoke(app, ["preprocess", "evidence", "--consolidate", "--json", "-c", str(config)]) assert result.exit_code == 1 assert "Missing Rule section" in json.loads(result.stdout)["error"] @pytest.mark.parametrize("flags", [["gc"], ["--dry-run"], ["--resume", "a" * 32]]) def test_cli_rejects_conflicting_consolidation_options_before_any_work(monkeypatch, flags): monkeypatch.setattr("tht.cli.preprocess_cmd.gc_from_config", lambda *a, **kw: pytest.fail("No cleanup")) result = CliRunner().invoke(app, ["preprocess", "evidence", "--consolidate", "--json", *flags]) assert result.exit_code == 2 assert json.loads(result.stdout)["code"] == "invalid_consolidation" def test_first_consolidation_reports_legacy_conversion_error(monkeypatch, tmp_path): from tht.evidence.authoring import EvidencePreparationError write_unit(tmp_path) (tmp_path / "evidence/manifest.yaml").write_text("invalid") monkeypatch.setattr("tht.config.load_config", lambda _: SimpleNamespace( evidence=SimpleNamespace(local_archive_root=tmp_path))) def fail(_): raise EvidencePreparationError("source_invalid", "source/guide.md") monkeypatch.setattr("tht.evidence.authoring.migrate_workspace_evidence", fail) with pytest.raises(ConsolidationError, match="source/guide.md"): consolidate_from_config(tmp_path / "config.yaml") def test_preprocessing_uses_only_the_active_snapshot_and_clear_preserves_primary_files(monkeypatch, tmp_path): import yaml from tht.cli.preprocess_cmd import clear_from_config from tht.config import load_config from tht.evidence.sources import build_sources path = write_unit(tmp_path / "local") archive = LocalEvidenceArchive(tmp_path / "local") archive.consolidate(actor="Alice", activate=lambda _: None) config = _runtime_config(tmp_path) raw = yaml.safe_load(config.read_text()) raw["evidence"]["local_archive_root"] = str(tmp_path / "local") config.write_text(yaml.safe_dump(raw)) path.write_text(path.read_text().replace("Use order ID and year.", "Unconsolidated text.")) cfg = load_config(config) sources = build_sources(cfg.evidence) assert len(sources) == 1 assert sources[0].root == archive.active_snapshot() saved = path.read_bytes() monkeypatch.setenv("THT_PROFILE", "server") monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda *a, **kw: SimpleNamespace(clear_reference=lambda: True)) clear_from_config(config) assert path.read_bytes() == saved assert archive.active_snapshot().is_dir() def test_manual_git_sequence_preserves_edits_additions_deletions_and_archive_metadata(tmp_path): import subprocess def git(root, *args): return subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True) remote = tmp_path / "remote.git" repo = tmp_path / "repo" repo.mkdir() git(tmp_path, "init", "--bare", str(remote)) git(repo, "init", "--initial-branch=main") git(repo, "config", "user.name", "Evidence test") git(repo, "config", "user.email", "evidence@example.invalid") git(repo, "remote", "add", "origin", str(remote)) root = repo / "workspace" path = write_unit(root) original = path.read_text() from tht.evidence.canonical import parse_curated_markdown identity = parse_curated_markdown(original).id removed = path.with_name("removed.md") removed.write_text(original.replace(identity, "evidence:removed")) archive = LocalEvidenceArchive(root) archive.consolidate(actor="Curator", activate=lambda _: None) git(repo, "add", "-A", "--", "workspace/evidence") git(repo, "commit", "--only", "-m", "Baseline", "--", "workspace/evidence") path.write_text(path.read_text().replace("Use order ID and year.", "Use the approved compound key.")) added = path.with_name("added.md") added.write_text(original.replace(identity, "evidence:added")) removed.unlink() archive.consolidate(actor="Curator", activate=lambda _: None) git(repo, "status", "--short") git(repo, "diff", "HEAD", "--", "workspace/evidence") git(repo, "add", "-A", "--", "workspace/evidence") git(repo, "commit", "--only", "-m", "Curate Evidence", "--", "workspace/evidence") git(repo, "push", "origin", "main") clone = tmp_path / "clone" git(tmp_path, "clone", "--branch", "main", str(remote), str(clone)) copy = clone / "workspace" assert (copy / path.relative_to(root)).read_bytes() == path.read_bytes() assert (copy / added.relative_to(root)).exists() assert not (copy / removed.relative_to(root)).exists() assert (copy / "evidence/local-manifest.yaml").read_bytes() == (root / "evidence/local-manifest.yaml").read_bytes() assert LocalEvidenceArchive(copy).active_snapshot().is_dir() assert {unit["status"] for unit in browse(copy, {})["items"]} == {"active"}