714 lines
29 KiB
Python
714 lines
29 KiB
Python
import hashlib
|
|
import subprocess
|
|
import threading
|
|
import unicodedata
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from tht.evidence import (
|
|
CuratedEvidence,
|
|
EvidenceManifest,
|
|
EvidencePreparationError,
|
|
EvidenceRestructurer,
|
|
RestructureCandidate,
|
|
dump_curated_markdown,
|
|
dump_manifest,
|
|
load_curated_tree,
|
|
load_manifest,
|
|
migrate_workspace_evidence,
|
|
prepare_workspace_evidence,
|
|
validate_workspace_evidence,
|
|
)
|
|
|
|
|
|
def _evidence(source_text: str, *, review_items=()):
|
|
normalized_source = unicodedata.normalize(
|
|
"NFC", source_text.replace("\r\n", "\n").replace("\r", "\n"),
|
|
)
|
|
if not normalized_source.endswith("\n"):
|
|
normalized_source += "\n"
|
|
return CuratedEvidence.model_validate({
|
|
"schema_version": 1,
|
|
"id": "evidence:fascia-pediatrica",
|
|
"title": "Fascia pediatrica",
|
|
"kind": "domain",
|
|
"purposes": ["disambiguation"],
|
|
"applies_to": {"concepts": ["fascia pediatrica"]},
|
|
"language": "it",
|
|
"provenance": {
|
|
"source_file": "source/domain/patient.md",
|
|
"source_sha256": "sha256:" + hashlib.sha256(normalized_source.encode()).hexdigest(),
|
|
"supporting_excerpts": ["I pazienti sotto i 18 anni sono pediatrici."],
|
|
},
|
|
"review_items": list(review_items),
|
|
"payload": {"rule": "La fascia pediatrica comprende i minori."},
|
|
})
|
|
|
|
|
|
def _write_workspace(root, evidence, source_text):
|
|
evidence_root = root / "evidence"
|
|
source_path = evidence_root / "source" / "domain" / "patient.md"
|
|
curated_path = evidence_root / "curated" / "domain" / "fascia-pediatrica.md"
|
|
source_path.parent.mkdir(parents=True)
|
|
curated_path.parent.mkdir(parents=True)
|
|
source_path.write_text(source_text, encoding="utf-8")
|
|
curated_path.write_text(dump_curated_markdown(evidence), encoding="utf-8")
|
|
(evidence_root / "manifest.yaml").write_text(dump_manifest(EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v1",
|
|
"sources": {
|
|
evidence.provenance.source_file: {
|
|
"sha256": evidence.provenance.source_sha256,
|
|
"units": [evidence.id],
|
|
},
|
|
},
|
|
"orphans": [],
|
|
})), encoding="utf-8")
|
|
|
|
|
|
def test_workspace_validation_allows_a_coherent_curated_unit(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is True
|
|
assert report.findings == ()
|
|
|
|
|
|
def test_workspace_validation_keeps_review_items_visible_and_blocks_publication(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text, review_items=[{
|
|
"code": "ambiguous_source_statement",
|
|
"message": "Il sorgente non chiarisce la data di riferimento.",
|
|
"field": "payload.rule",
|
|
}]), source_text)
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert [finding.code for finding in report.findings] == ["unresolved_review_item"]
|
|
|
|
|
|
def test_manifest_round_trip_is_versioned_and_deterministic(tmp_path):
|
|
manifest = EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v1",
|
|
"sources": {
|
|
"source/domain/patient.md": {
|
|
"sha256": "sha256:" + "a" * 64,
|
|
"units": ["evidence:fascia-pediatrica"],
|
|
},
|
|
},
|
|
"orphans": [],
|
|
})
|
|
path = tmp_path / "evidence" / "manifest.yaml"
|
|
path.parent.mkdir(parents=True)
|
|
path.write_text(dump_manifest(manifest), encoding="utf-8")
|
|
|
|
loaded = load_manifest(path)
|
|
|
|
assert loaded == manifest
|
|
assert path.read_text(encoding="utf-8") == dump_manifest(manifest)
|
|
|
|
|
|
def test_manifest_rejects_unknown_fields_and_incompatible_pipeline_version():
|
|
with pytest.raises(ValidationError):
|
|
EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v2",
|
|
"sources": {},
|
|
"orphans": [],
|
|
"approved": True,
|
|
})
|
|
|
|
|
|
def test_workspace_validation_requires_a_manifest(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
(tmp_path / "evidence" / "manifest.yaml").unlink()
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert [finding.code for finding in report.findings] == ["manifest_missing"]
|
|
|
|
|
|
def test_workspace_validation_reports_a_non_utf8_manifest_without_raising(tmp_path):
|
|
evidence_root = tmp_path / "evidence"
|
|
evidence_root.mkdir()
|
|
(evidence_root / "manifest.yaml").write_bytes(b"\xff")
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert [finding.code for finding in report.findings] == ["manifest_invalid"]
|
|
|
|
|
|
def test_workspace_validation_reports_invalid_curated_documents_without_raising(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
(tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md").write_text(
|
|
"not canonical frontmatter", encoding="utf-8",
|
|
)
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert [finding.code for finding in report.findings] == ["curated_invalid"]
|
|
|
|
|
|
def test_workspace_validation_preserves_manifest_orphans_but_blocks_publication(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
manifest = load_manifest(manifest_path).model_copy(update={"orphans": ("evidence:retired",)})
|
|
manifest_path.write_text(dump_manifest(manifest), encoding="utf-8")
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert [finding.code for finding in report.findings] == ["orphaned_unit"]
|
|
|
|
|
|
def test_workspace_validation_requires_manifest_and_provenance_to_agree(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
evidence = _evidence(source_text)
|
|
_write_workspace(tmp_path, evidence, source_text)
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
manifest = EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v1",
|
|
"sources": {
|
|
evidence.provenance.source_file: {
|
|
"sha256": "sha256:" + "b" * 64,
|
|
"units": ["evidence:another-unit"],
|
|
},
|
|
},
|
|
"orphans": [],
|
|
})
|
|
manifest_path.write_text(dump_manifest(manifest), encoding="utf-8")
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert {finding.code for finding in report.findings} == {
|
|
"manifest_source_hash_mismatch",
|
|
"manifest_unit_missing",
|
|
"source_hash_mismatch",
|
|
"manifest_unit_without_curated",
|
|
}
|
|
|
|
|
|
def test_workspace_validation_checks_unreferenced_manifest_sources_and_units(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
manifest = EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v1",
|
|
"sources": {
|
|
"source/domain/missing.md": {
|
|
"sha256": "sha256:" + "a" * 64,
|
|
"units": ["evidence:removed"],
|
|
},
|
|
},
|
|
"orphans": [],
|
|
})
|
|
manifest_path.write_text(dump_manifest(manifest), encoding="utf-8")
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert {finding.code for finding in report.findings} == {
|
|
"manifest_source_missing",
|
|
"source_missing",
|
|
"manifest_unit_without_curated",
|
|
}
|
|
|
|
|
|
def test_manifest_rejects_unsafe_source_paths_and_legacy_unit_identifiers():
|
|
with pytest.raises(ValidationError):
|
|
EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v1",
|
|
"sources": {"../patient.pdf": {"sha256": "sha256:" + "a" * 64, "units": ["domain:patient"]}},
|
|
"orphans": [],
|
|
})
|
|
|
|
|
|
def test_manifest_rejects_a_unit_declared_by_two_sources():
|
|
with pytest.raises(ValidationError, match="only one source"):
|
|
EvidenceManifest.model_validate({
|
|
"schema_version": 1,
|
|
"pipeline_version": "evidence-authoring-v1",
|
|
"sources": {
|
|
"source/domain/one.md": {
|
|
"sha256": "sha256:" + "a" * 64,
|
|
"units": ["evidence:shared"],
|
|
},
|
|
"source/domain/two.md": {
|
|
"sha256": "sha256:" + "b" * 64,
|
|
"units": ["evidence:shared"],
|
|
},
|
|
},
|
|
"orphans": [],
|
|
})
|
|
|
|
|
|
def test_workspace_validation_rejects_duplicate_curated_ids_and_changed_sources(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
evidence = _evidence(source_text)
|
|
_write_workspace(tmp_path, evidence, source_text)
|
|
duplicate_path = tmp_path / "evidence" / "curated" / "domain" / "duplicate.md"
|
|
duplicate_path.write_text(dump_curated_markdown(evidence), encoding="utf-8")
|
|
(tmp_path / "evidence" / "source" / "domain" / "patient.md").write_text(
|
|
"Il testo sorgente è cambiato.", encoding="utf-8",
|
|
)
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert report.publishable is False
|
|
assert {finding.code for finding in report.findings} == {
|
|
"duplicate_evidence_id",
|
|
"source_hash_mismatch",
|
|
"supporting_excerpt_missing",
|
|
}
|
|
|
|
|
|
def test_workspace_validation_reports_unreadable_or_oversized_sources(tmp_path, monkeypatch):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
source_path.write_bytes(b"\xff")
|
|
|
|
unreadable = validate_workspace_evidence(tmp_path)
|
|
|
|
assert [finding.code for finding in unreadable.findings] == ["source_unreadable"]
|
|
|
|
source_path.write_text(source_text, encoding="utf-8")
|
|
monkeypatch.setattr("tht.evidence.authoring.MAX_AUTHORING_FILE_BYTES", 1)
|
|
|
|
oversized = validate_workspace_evidence(tmp_path)
|
|
|
|
assert [finding.code for finding in oversized.findings] == ["source_oversized"]
|
|
|
|
|
|
def test_workspace_validation_rejects_a_source_symlink(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
outside = tmp_path / "outside.md"
|
|
outside.write_text(source_text, encoding="utf-8")
|
|
source_path.unlink()
|
|
source_path.symlink_to(outside)
|
|
|
|
report = validate_workspace_evidence(tmp_path)
|
|
|
|
assert [finding.code for finding in report.findings] == ["source_unsafe"]
|
|
|
|
|
|
class _Restructurer(EvidenceRestructurer):
|
|
def __init__(self, candidates):
|
|
self.candidates = candidates
|
|
self.requests = []
|
|
|
|
def restructure(self, request):
|
|
self.requests.append(request)
|
|
return tuple(self.candidates)
|
|
|
|
|
|
def _candidate(*, title="Fascia pediatrica", existing_id=None, kind="domain", payload=None):
|
|
return RestructureCandidate.model_validate({
|
|
"schema_version": 1,
|
|
"existing_id": existing_id,
|
|
"title": title,
|
|
"kind": kind,
|
|
"purposes": ["disambiguation"],
|
|
"applies_to": {"concepts": ["fascia pediatrica"]},
|
|
"language": "it",
|
|
"supporting_excerpts": ["I pazienti sotto i 18 anni sono pediatrici."],
|
|
"review_items": [],
|
|
"payload": payload or {"rule": "La fascia pediatrica comprende i minori."},
|
|
})
|
|
|
|
|
|
def test_prepare_changed_source_uses_one_model_call_and_applies_a_valid_batch(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
source_path.write_text(source_text + " La regola è revisionata.\n", encoding="utf-8")
|
|
restructurer = _Restructurer([_candidate(existing_id="evidence:fascia-pediatrica")])
|
|
|
|
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert report.model_calls == 1
|
|
assert report.changed == ("source/domain/patient.md",)
|
|
assert report.created == ()
|
|
assert len(restructurer.requests) == 1
|
|
assert restructurer.requests[0].previous_units[0].id == "evidence:fascia-pediatrica"
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
curated = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
|
assert curated.schema_version == 3
|
|
assert "# Fascia pediatrica\n" in curated_path.read_text(encoding="utf-8")
|
|
assert validate_workspace_evidence(tmp_path).publishable is True
|
|
|
|
|
|
def test_migrate_workspace_evidence_rewrites_v1_units_as_v3_without_a_model_call(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
|
|
report = migrate_workspace_evidence(tmp_path, git_status=lambda _: ())
|
|
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
migrated = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
|
assert report.migrated == ("evidence:fascia-pediatrica",)
|
|
assert report.unchanged == ()
|
|
assert migrated.schema_version == 3
|
|
assert migrated.payload.rule == "La fascia pediatrica comprende i minori."
|
|
assert "## Regola\n\nLa fascia pediatrica comprende i minori." in curated_path.read_text(
|
|
encoding="utf-8",
|
|
)
|
|
assert report.findings == ()
|
|
|
|
|
|
def test_migrate_workspace_evidence_rewrites_v2_units_as_table_free_v3(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
evidence = _evidence(source_text).model_copy(update={"schema_version": 2})
|
|
_write_workspace(tmp_path, evidence, source_text)
|
|
|
|
first = migrate_workspace_evidence(tmp_path, git_status=lambda _: ())
|
|
second = migrate_workspace_evidence(tmp_path, git_status=lambda _: ())
|
|
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
text = curated_path.read_text(encoding="utf-8")
|
|
migrated = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
|
assert first.migrated == ("evidence:fascia-pediatrica",)
|
|
assert first.unchanged == ()
|
|
assert second.migrated == ()
|
|
assert second.unchanged == ("evidence:fascia-pediatrica",)
|
|
assert migrated.schema_version == 3
|
|
assert text.startswith("<!-- tht:metadata:")
|
|
assert not any(line.startswith("|") for line in text.splitlines())
|
|
|
|
|
|
def test_migrate_workspace_evidence_rejects_dirty_curated_files_in_a_nested_workspace(tmp_path):
|
|
subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True)
|
|
workspace_root = tmp_path / "psd-clinical"
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(workspace_root, _evidence(source_text), source_text)
|
|
|
|
with pytest.raises(EvidencePreparationError, match="authoring_worktree_dirty"):
|
|
migrate_workspace_evidence(workspace_root)
|
|
|
|
|
|
def test_prepare_can_issue_independent_source_calls_concurrently(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_root = tmp_path / "evidence" / "source" / "domain"
|
|
changed_text = source_text + "\nRegola revisionata.\n"
|
|
(source_root / "patient.md").write_text(changed_text, encoding="utf-8")
|
|
(source_root / "second.md").write_text(changed_text, encoding="utf-8")
|
|
barrier = threading.Barrier(2)
|
|
|
|
class ConcurrentRestructurer:
|
|
def __init__(self):
|
|
self.requests = []
|
|
|
|
def restructure(self, request):
|
|
self.requests.append(request)
|
|
barrier.wait(timeout=2)
|
|
return (_candidate(title=f"Unit {request.source_file}"),)
|
|
|
|
restructurer = ConcurrentRestructurer()
|
|
|
|
report = prepare_workspace_evidence(
|
|
tmp_path,
|
|
restructurer=restructurer,
|
|
git_status=lambda _: (),
|
|
max_workers=2,
|
|
)
|
|
|
|
assert report.model_calls == 2
|
|
assert sorted(request.source_file for request in restructurer.requests) == [
|
|
"source/domain/patient.md",
|
|
"source/domain/second.md",
|
|
]
|
|
|
|
|
|
def test_prepare_unchanged_source_skips_model_and_does_not_write(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
original = curated_path.read_text(encoding="utf-8")
|
|
restructurer = _Restructurer([])
|
|
|
|
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert report.model_calls == 0
|
|
assert report.unchanged == ("source/domain/patient.md",)
|
|
assert curated_path.read_text(encoding="utf-8") == original
|
|
assert restructurer.requests == []
|
|
|
|
|
|
def test_prepare_rejects_dirty_curated_state_before_model_call(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
restructurer = _Restructurer([])
|
|
|
|
with pytest.raises(EvidencePreparationError, match="authoring_worktree_dirty"):
|
|
prepare_workspace_evidence(
|
|
tmp_path,
|
|
restructurer=restructurer,
|
|
git_status=lambda _: (" M evidence/curated/domain/patient.md",),
|
|
)
|
|
|
|
assert restructurer.requests == []
|
|
|
|
|
|
def test_prepare_refuses_an_incompatible_pipeline_version_without_writing(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
manifest_path.write_text(
|
|
manifest_path.read_text(encoding="utf-8").replace("evidence-authoring-v1", "evidence-authoring-v2"),
|
|
encoding="utf-8",
|
|
)
|
|
original_manifest = manifest_path.read_text(encoding="utf-8")
|
|
original_curated = curated_path.read_text(encoding="utf-8")
|
|
|
|
with pytest.raises(EvidencePreparationError, match="pipeline_upgrade_required"):
|
|
prepare_workspace_evidence(tmp_path, restructurer=_Restructurer([]), git_status=lambda _: ())
|
|
|
|
assert manifest_path.read_text(encoding="utf-8") == original_manifest
|
|
assert curated_path.read_text(encoding="utf-8") == original_curated
|
|
|
|
|
|
def test_prepare_preserves_an_id_when_reclassified_and_allocates_a_new_id_for_a_split(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
source_path.write_text(source_text + " I pazienti sopra i 18 anni sono adulti.\n", encoding="utf-8")
|
|
restructurer = _Restructurer([
|
|
_candidate(
|
|
existing_id="evidence:fascia-pediatrica",
|
|
kind="glossary",
|
|
payload={"definition": "Paziente con meno di 18 anni."},
|
|
),
|
|
_candidate(title="Fascia adulta"),
|
|
])
|
|
|
|
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert report.created == ("evidence:fascia-adulta",)
|
|
documents = {document.id: document for document in load_curated_tree(tmp_path / "evidence" / "curated")}
|
|
assert documents["evidence:fascia-pediatrica"].kind == "glossary"
|
|
assert documents["evidence:fascia-adulta"].kind == "domain"
|
|
|
|
|
|
def test_prepare_retains_units_from_a_removed_source_as_blocking_orphans(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
(tmp_path / "evidence" / "source" / "domain" / "patient.md").unlink()
|
|
restructurer = _Restructurer([])
|
|
|
|
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert report.model_calls == 0
|
|
assert report.orphaned == ("evidence:fascia-pediatrica",)
|
|
assert (tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md").is_file()
|
|
assert [finding.code for finding in report.findings] == ["orphaned_unit"]
|
|
|
|
|
|
def test_prepare_preserves_ids_without_orphans_for_a_unique_source_rename(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_root = tmp_path / "evidence" / "source" / "domain"
|
|
(source_root / "patient.md").rename(source_root / "patient-age.md")
|
|
restructurer = _Restructurer([])
|
|
|
|
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert report.model_calls == 0
|
|
assert report.orphaned == ()
|
|
assert report.unchanged == ("source/domain/patient-age.md",)
|
|
assert validate_workspace_evidence(tmp_path).publishable is True
|
|
|
|
|
|
def test_prepare_rejects_an_unknown_existing_id_without_writing_the_batch(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
source_path.write_text(source_text + " Revisione.\n", encoding="utf-8")
|
|
original = (tmp_path / "evidence" / "manifest.yaml").read_text(encoding="utf-8")
|
|
restructurer = _Restructurer([_candidate(existing_id="evidence:not-supplied")])
|
|
|
|
with pytest.raises(EvidencePreparationError, match="unknown_existing_id"):
|
|
prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert (tmp_path / "evidence" / "manifest.yaml").read_text(encoding="utf-8") == original
|
|
|
|
|
|
def test_prepare_marks_an_omitted_prior_unit_for_human_review(tmp_path):
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
(tmp_path / "evidence" / "source" / "domain" / "patient.md").write_text(
|
|
"La classificazione pediatrica non è documentata.\n", encoding="utf-8",
|
|
)
|
|
restructurer = _Restructurer([])
|
|
|
|
report = prepare_workspace_evidence(tmp_path, restructurer=restructurer, git_status=lambda _: ())
|
|
|
|
assert report.model_calls == 1
|
|
assert [finding.code for finding in report.findings] == [
|
|
"supporting_excerpt_missing", "unresolved_review_item",
|
|
]
|
|
retained = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
|
assert retained.schema_version == 3
|
|
assert retained.review_items[0].code == "source_no_longer_supports_unit"
|
|
|
|
|
|
def test_resolve_retires_an_orphan_and_updates_curated_content_and_manifest_atomically(tmp_path):
|
|
from tht.evidence import resolve_workspace_evidence
|
|
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
source_path.unlink()
|
|
prepare_workspace_evidence(tmp_path, restructurer=_Restructurer([]), git_status=lambda _: ())
|
|
|
|
report = resolve_workspace_evidence(
|
|
tmp_path,
|
|
"evidence:fascia-pediatrica",
|
|
retire=True,
|
|
git_status=lambda _: (),
|
|
)
|
|
|
|
assert report.action == "retired"
|
|
assert report.evidence_id == "evidence:fascia-pediatrica"
|
|
assert not (tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md").exists()
|
|
manifest = load_manifest(tmp_path / "evidence" / "manifest.yaml")
|
|
assert manifest.orphans == ()
|
|
assert manifest.sources == {}
|
|
|
|
|
|
def test_resolve_relinks_an_unit_and_updates_its_manifest_membership(tmp_path):
|
|
from tht.evidence import resolve_workspace_evidence
|
|
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
next_source = tmp_path / "evidence" / "source" / "domain" / "patient-age.md"
|
|
next_source.write_text(source_text, encoding="utf-8")
|
|
|
|
report = resolve_workspace_evidence(
|
|
tmp_path,
|
|
"evidence:fascia-pediatrica",
|
|
source="source/domain/patient-age.md",
|
|
git_status=lambda _: (),
|
|
)
|
|
|
|
assert report.action == "relinked"
|
|
assert report.source_file == "source/domain/patient-age.md"
|
|
document = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
|
assert document.provenance.source_file == "source/domain/patient-age.md"
|
|
manifest = load_manifest(tmp_path / "evidence" / "manifest.yaml")
|
|
assert manifest.sources["source/domain/patient.md"].units == ()
|
|
assert manifest.sources["source/domain/patient-age.md"].units == ("evidence:fascia-pediatrica",)
|
|
assert validate_workspace_evidence(tmp_path).publishable is True
|
|
|
|
|
|
def test_resolve_relink_clears_an_unsupported_review_item_when_the_new_source_supports_it(tmp_path):
|
|
from tht.evidence import resolve_workspace_evidence
|
|
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
source_path = tmp_path / "evidence" / "source" / "domain" / "patient.md"
|
|
source_path.write_text("La classificazione pediatrica non è documentata.\n", encoding="utf-8")
|
|
prepare_workspace_evidence(tmp_path, restructurer=_Restructurer([]), git_status=lambda _: ())
|
|
replacement_source = tmp_path / "evidence" / "source" / "domain" / "patient-revised.md"
|
|
replacement_source.write_text(source_text, encoding="utf-8")
|
|
|
|
report = resolve_workspace_evidence(
|
|
tmp_path,
|
|
"evidence:fascia-pediatrica",
|
|
source="source/domain/patient-revised.md",
|
|
git_status=lambda _: (),
|
|
)
|
|
|
|
assert report.findings == ()
|
|
document = load_curated_tree(tmp_path / "evidence" / "curated")[0]
|
|
assert all(item.code != "source_no_longer_supports_unit" for item in document.review_items)
|
|
assert validate_workspace_evidence(tmp_path).publishable is True
|
|
|
|
|
|
def test_resolve_refuses_an_unsafe_relink_without_writing(tmp_path):
|
|
from tht.evidence import EvidencePreparationError, resolve_workspace_evidence
|
|
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
original_curated = curated_path.read_text(encoding="utf-8")
|
|
original_manifest = manifest_path.read_text(encoding="utf-8")
|
|
|
|
with pytest.raises(EvidencePreparationError, match="source_invalid"):
|
|
resolve_workspace_evidence(
|
|
tmp_path,
|
|
"evidence:fascia-pediatrica",
|
|
source="../outside.md",
|
|
git_status=lambda _: (),
|
|
)
|
|
|
|
assert curated_path.read_text(encoding="utf-8") == original_curated
|
|
assert manifest_path.read_text(encoding="utf-8") == original_manifest
|
|
|
|
|
|
def test_resolve_refuses_a_relink_through_a_symlinked_source_directory(tmp_path):
|
|
from tht.evidence import EvidencePreparationError, resolve_workspace_evidence
|
|
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
original_curated = curated_path.read_text(encoding="utf-8")
|
|
original_manifest = manifest_path.read_text(encoding="utf-8")
|
|
source_root = tmp_path / "evidence" / "source"
|
|
outside_source = tmp_path / "outside-source"
|
|
source_root.rename(outside_source)
|
|
source_root.symlink_to(outside_source, target_is_directory=True)
|
|
|
|
with pytest.raises(EvidencePreparationError, match="source_invalid"):
|
|
resolve_workspace_evidence(
|
|
tmp_path,
|
|
"evidence:fascia-pediatrica",
|
|
source="source/domain/patient.md",
|
|
git_status=lambda _: (),
|
|
)
|
|
|
|
assert curated_path.read_text(encoding="utf-8") == original_curated
|
|
assert manifest_path.read_text(encoding="utf-8") == original_manifest
|
|
|
|
|
|
def test_resolve_rejects_any_dirty_worktree_without_writing(tmp_path):
|
|
from tht.evidence import EvidencePreparationError, resolve_workspace_evidence
|
|
|
|
source_text = "I pazienti sotto i 18 anni sono pediatrici."
|
|
_write_workspace(tmp_path, _evidence(source_text), source_text)
|
|
curated_path = tmp_path / "evidence" / "curated" / "domain" / "fascia-pediatrica.md"
|
|
manifest_path = tmp_path / "evidence" / "manifest.yaml"
|
|
original_curated = curated_path.read_text(encoding="utf-8")
|
|
original_manifest = manifest_path.read_text(encoding="utf-8")
|
|
|
|
with pytest.raises(EvidencePreparationError, match="worktree_dirty"):
|
|
resolve_workspace_evidence(
|
|
tmp_path,
|
|
"evidence:fascia-pediatrica",
|
|
retire=True,
|
|
git_status=lambda _: (" M README.md",),
|
|
)
|
|
|
|
assert curated_path.read_text(encoding="utf-8") == original_curated
|
|
assert manifest_path.read_text(encoding="utf-8") == original_manifest
|