Files
ThothII/harness/tests/test_evidence_imports.py
T
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

240 lines
10 KiB
Python

import json
import pytest
from test_evidence_local_archive import read_active, write_unit
from tht.evidence.adapters import FilesystemEvidenceSource
from tht.evidence.authoring import RestructureCandidate
from tht.evidence.canonical import EvidenceProvenance, ManualEvidenceProvenance
from tht.evidence.contracts import AcquiredDocument, SourceObject
from tht.evidence.imports import decide, refresh, reviews
from tht.evidence.local_archive import ArchiveConflict, LocalEvidenceArchive, _digest
class Refiner:
def __init__(self):
self.calls = []
def restructure(self, request):
self.calls.append(request)
return (RestructureCandidate(schema_version=1,
existing_id=request.previous_units[0].id if request.previous_units else None,
title="Order key", kind="domain", language="en", purposes=("sql_generation",),
payload={"rule": request.normalized_text.strip()}, supporting_excerpts=(request.normalized_text.strip(),)),)
class Remote:
def __init__(self, uri="https://docs.example.test/rule.md"):
self.uri = uri
self.text = "Use order ID and year."
self.calls = 0
self.fail = False
self.absent = False
def discover(self):
self.calls += 1
if self.fail:
raise RuntimeError("private access credential must not leak")
if not self.absent:
yield SourceObject(source_id="test:rule", uri=self.uri,
fingerprint="sha256:" + _digest(self.text.encode()))
def acquire(self, item):
self.calls += 1
return AcquiredDocument(source=item, content=self.text.encode(), media_type="text/markdown")
def empty_archive(tmp_path):
(tmp_path / "evidence/curated").mkdir(parents=True)
archive = LocalEvidenceArchive(tmp_path)
archive.initialize()
return archive
def choose(archive, choice="replace", activate=lambda _: None):
row = next(r for r in reviews(archive) if r["status"] in {"review", "applying"})
return decide(archive, source_id=row["id"], revision=row["revision"], decision=choice,
actor="Curator", activate=activate)
def test_local_source_refresh_preserves_manual_correction_and_explicit_choices(tmp_path):
path = write_unit(tmp_path, source=True)
archive = LocalEvidenceArchive(tmp_path)
archive.consolidate(actor="Curator", activate=lambda _: None)
source = FilesystemEvidenceSource(archive.evidence, patterns=("source/*.md",))
refiner = Refiner()
assert refresh(archive, [source], refiner)["counts"]["unchanged"] == 1
assert not refiner.calls
path.write_text(path.read_text().replace("## Rule\n\nUse order ID and year.", "## Rule\n\nUse company as well."))
archive.consolidate(actor="Curator", activate=lambda _: None)
original = archive.active_snapshot()
(archive.evidence / "source/orders.md").write_text("The new source says use ID only.")
refresh(archive, [source], refiner)
assert archive.active_snapshot() == original
assert reviews(archive)[0]["current"][0]["payload"]["rule"] == "Use company as well."
choose(archive, "keep")
unit = read_active(archive)
assert unit.payload.rule == "Use company as well."
assert isinstance(unit.provenance, ManualEvidenceProvenance)
assert unit.provenance.original.supporting_excerpts == ("Use order ID and year.",)
assert refresh(archive, [source], refiner)["counts"]["unchanged"] == 1
(archive.evidence / "source/orders.md").write_text("Now use ID, year and company.")
refresh(archive, [source], refiner)
choose(archive)
unit = read_active(archive)
assert unit.payload.rule == "Now use ID, year and company."
assert isinstance(unit.provenance, EvidenceProvenance)
assert (archive.active_snapshot() / unit.provenance.source_file).read_text().strip() == unit.payload.rule
assert (original / "curated/domain/order-key.md").exists()
@pytest.mark.parametrize("uri", ["https://docs.example.test/rule.md", "s3://documents/rule.md"])
def test_read_only_remote_import_preserves_bytes_and_never_refreshes_implicitly(tmp_path, uri):
archive = empty_archive(tmp_path)
remote, refiner = Remote(uri), Refiner()
refresh(archive, [remote], refiner)
assert archive.active_snapshot() is None
choose(archive)
assert read_active(archive).payload.rule == remote.text
assert remote.calls == 2
archive.consolidate(actor="Curator", activate=lambda _: None)
assert remote.calls == 2
assert refresh(archive, [remote], refiner)["counts"]["unchanged"] == 1
assert len(refiner.calls) == 1
saved = next((archive.metadata / "acquisitions").rglob("*.json"))
assert json.loads(saved.read_text())["source"]["uri"] == uri
assert json.loads(saved.read_text())["raw_base64"]
def test_access_failure_and_missing_source_do_not_remove_or_replace_curated_content(tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
choose(archive)
before = (archive.metadata / "sources.json").read_bytes()
active = archive.active_snapshot()
remote.fail = True
with pytest.raises(RuntimeError):
refresh(archive, [remote], refiner)
assert (archive.metadata / "sources.json").read_bytes() == before
remote.fail, remote.absent = False, True
refresh(archive, [remote], refiner)
assert reviews(archive)[0]["availability"] == "missing"
assert archive.active_snapshot() == active
def test_refresh_does_not_resurrect_deleted_units_with_new_model_ids(tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
choose(archive)
(archive.evidence / "curated/domain/order-key.md").unlink()
archive.consolidate(actor="Curator", activate=lambda _: None)
remote.text = "Changed source could regenerate the deleted rule."
refresh(archive, [remote], refiner)
row = reviews(archive)[0]
assert row["suppressed"] and row["proposed"] == []
choose(archive)
assert archive._files(archive.active_snapshot()) == {}
def test_failed_activation_retries_saved_source_decision_without_reacquisition(tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
def fail(_):
raise RuntimeError("offline index")
with pytest.raises(RuntimeError, match="offline index"):
choose(archive, activate=fail)
assert reviews(archive)[0]["status"] == "applying"
assert archive.active_snapshot() is None
choose(archive)
assert read_active(archive).payload.rule == remote.text
assert remote.calls == 2
def test_source_retirement_decision_also_suppresses_future_regeneration(tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
choose(archive)
remote.text = "The source no longer supports the old rule."
class EmptyRefiner:
def restructure(self, request):
return ()
refresh(archive, [remote], EmptyRefiner())
assert reviews(archive)[0]["removed_ids"] == ["evidence:order-key"]
choose(archive)
remote.text = "A newly worded source could restore the same rule."
refresh(archive, [remote], refiner)
assert reviews(archive)[0]["proposed"] == []
def test_retry_never_overwrites_an_intervening_external_edit(tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
def fail(_):
raise RuntimeError("offline index")
with pytest.raises(RuntimeError):
choose(archive, activate=fail)
path = archive.evidence / "curated/domain/order-key.md"
edited = path.read_text().replace("Use order ID and year.", "External correction.")
path.write_text(edited)
with pytest.raises(ArchiveConflict, match="changed during"):
choose(archive)
assert path.read_text() == edited
assert archive.active_snapshot() is None
def test_external_edit_invalidates_comparison_and_same_source_can_be_reviewed_again(tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
choose(archive)
remote.text = "New source."
refresh(archive, [remote], refiner)
old_revision = reviews(archive)[0]["revision"]
path = archive.evidence / "curated/domain/order-key.md"
path.write_text(path.read_text().replace("## Rule\n\nUse order ID and year.", "## Rule\n\nManual correction."))
with pytest.raises(ArchiveConflict, match="changed since"):
choose(archive)
refresh(archive, [remote], refiner)
assert reviews(archive)[0]["revision"] != old_revision
choose(archive, "keep")
assert read_active(archive).payload.rule == "Manual correction."
def test_decision_recovers_interruption_between_journal_and_archive_write(monkeypatch, tmp_path):
archive, remote, refiner = empty_archive(tmp_path), Remote(), Refiner()
refresh(archive, [remote], refiner)
write = archive._write_state
monkeypatch.setattr(archive, "_write_state", lambda _: (_ for _ in ()).throw(OSError("interrupted")))
with pytest.raises(OSError):
choose(archive)
monkeypatch.setattr(archive, "_write_state", write)
choose(archive)
assert read_active(archive).payload.rule == remote.text
def test_local_incoming_draft_needs_no_database_or_source_server(tmp_path):
archive = empty_archive(tmp_path)
(archive.evidence / "incoming").mkdir()
(archive.evidence / "incoming/draft.md").write_text("A new domain rule.")
refresh(archive, [FilesystemEvidenceSource(archive.evidence, patterns=("incoming/*.md",))], Refiner())
choose(archive)
assert read_active(archive).payload.rule == "A new domain rule."
def test_source_cli_access_error_is_sanitized_json(monkeypatch, tmp_path):
from typer.testing import CliRunner
from tht.cli import app
def fail(*a, **kw):
raise RuntimeError("private credential")
monkeypatch.setattr("tht.evidence.administration.source_action", fail)
result = CliRunner().invoke(app, ["evidence", "sources", "refresh", "--json", "-c", str(tmp_path / "cfg")])
assert result.exit_code == 1
assert json.loads(result.stdout)["status"] == "failed"
assert "private credential" not in result.stdout
def test_installed_refiner_uses_deployment_resources_outside_the_python_wheel(monkeypatch, tmp_path):
from tht.evidence.authoring import authoring_skill_path
monkeypatch.setenv("THT_HARNESS_DIR", str(tmp_path / "deployment"))
assert authoring_skill_path() == tmp_path / "deployment/.pi/skills/tht-evidence-authoring/SKILL.md"