Publish documentation / publish (push) Successful in 1m27s
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.
156 lines
7.4 KiB
Python
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"}
|