132 lines
4.2 KiB
Python
132 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "run_accuracy_phase3_full_data_scan.py"
|
|
|
|
|
|
def run_scan(
|
|
repo: Path,
|
|
output: Path,
|
|
*,
|
|
resume: bool = False,
|
|
unreachable_scopes: tuple[str, ...] = (),
|
|
) -> dict:
|
|
command = [
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--repo-root",
|
|
str(repo),
|
|
"--output-dir",
|
|
str(output),
|
|
"--roots",
|
|
"data",
|
|
"--batch-size",
|
|
"2",
|
|
]
|
|
if resume:
|
|
command.append("--resume")
|
|
for scope in unreachable_scopes:
|
|
command.extend(("--unreachable-scope", scope))
|
|
completed = subprocess.run(command, check=True, capture_output=True, text=True)
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def test_phase3_scan_reconciles_and_resumes_deterministically(tmp_path: Path) -> None:
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
(data / "valid.geojson").write_text(
|
|
json.dumps(
|
|
{
|
|
"type": "FeatureCollection",
|
|
"features": [
|
|
{
|
|
"type": "Feature",
|
|
"properties": {"source": "GRB"},
|
|
"geometry": {"type": "Point", "coordinates": [4.4, 50.8]},
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
invalid = data / "invalid.geojson"
|
|
invalid.write_text(
|
|
'{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[0,0],[1,1],[1,0],[0,1],[0,0]]]}}]}',
|
|
encoding="utf-8",
|
|
)
|
|
duplicate_payload = '{"schema_version":1,"value":"same"}'
|
|
(data / "one.json").write_text(duplicate_payload, encoding="utf-8")
|
|
(data / "two.json").write_text(duplicate_payload, encoding="utf-8")
|
|
(data / "broken.tif").write_bytes(b"not a geotiff")
|
|
|
|
output = tmp_path / "evidence"
|
|
unreachable_scopes = (
|
|
"external://tower-corpora=Tower corpora are not mounted in this fixture",
|
|
"external://mounted-model-volumes=Model volumes are not mounted in this fixture",
|
|
"external://production-postgis-or-api=Production database is not configured in this fixture",
|
|
)
|
|
first = run_scan(tmp_path, output, unreachable_scopes=unreachable_scopes)
|
|
second = run_scan(
|
|
tmp_path,
|
|
output,
|
|
resume=True,
|
|
unreachable_scopes=unreachable_scopes,
|
|
)
|
|
manifest = json.loads((output / "full-scan-manifest.json").read_text(encoding="utf-8"))
|
|
quarantine = json.loads((output / "quarantine-manifest.json").read_text(encoding="utf-8"))
|
|
|
|
assert first["reconciliation"] == {"examined": 5, "skipped": 0, "unreachable": 3, "inventory_total": 8, "reconciles": True}
|
|
assert second["content_hash"] == first["content_hash"]
|
|
assert manifest["determinism"]["content_hash"] == first["content_hash"]
|
|
assert any(item["path"] == "data/broken.tif" for item in quarantine["items"])
|
|
assert any(item["path"] == "data/invalid.geojson" for item in quarantine["items"])
|
|
assert len(manifest["duplicates"]["exact_duplicate_groups"]) == 1
|
|
|
|
|
|
def test_phase3_scan_does_not_invent_unreachable_production_boundaries(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
(data / "present.json").write_text("{}", encoding="utf-8")
|
|
output = tmp_path / "evidence"
|
|
|
|
result = run_scan(tmp_path, output)
|
|
|
|
assert result["reconciliation"] == {
|
|
"examined": 1,
|
|
"skipped": 0,
|
|
"unreachable": 0,
|
|
"inventory_total": 1,
|
|
"reconciles": True,
|
|
}
|
|
|
|
|
|
def test_phase3_scan_records_a_missing_requested_root(tmp_path: Path) -> None:
|
|
output = tmp_path / "evidence"
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--repo-root",
|
|
str(tmp_path),
|
|
"--output-dir",
|
|
str(output),
|
|
"--roots",
|
|
"missing-data",
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
result = json.loads(completed.stdout)
|
|
manifest = json.loads((output / "full-scan-manifest.json").read_text(encoding="utf-8"))
|
|
|
|
assert result["reconciliation"]["unreachable"] == 1
|
|
assert manifest["items"][0]["path"] == "root://missing-data"
|