Files
ThothII/harness/tests/test_evidence_administration.py
Codex 82e2c91f42
Publish documentation / publish (push) Successful in 1m27s
feat: implement memory and evidence administration with guided repairs
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation.

Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
2026-09-10 10:31:34 +02:00

156 lines
7.4 KiB
Python

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"}