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.
240 lines
10 KiB
Python
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"
|