Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from hashlib import sha256
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = ROOT / "scripts" / "migrate_runtime_model_provenance.py"
|
||||
SPEC = importlib.util.spec_from_file_location("migrate_runtime_model_provenance", SCRIPT)
|
||||
assert SPEC and SPEC.loader
|
||||
module = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(module)
|
||||
|
||||
|
||||
def _sha(value: bytes) -> str:
|
||||
return sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _args(tmp_path: Path):
|
||||
model = tmp_path / "active.pt"
|
||||
checkpoint = tmp_path / "best.pt"
|
||||
base_model = tmp_path / "base.pt"
|
||||
training_args = tmp_path / "args.yaml"
|
||||
training_results = tmp_path / "results.csv"
|
||||
dataset_summary = tmp_path / "dataset-summary.json"
|
||||
dataset_yaml = tmp_path / "dataset.yaml"
|
||||
training_summary = tmp_path / "training-summary.json"
|
||||
|
||||
model.write_bytes(b"exact promoted model bytes")
|
||||
checkpoint.write_bytes(model.read_bytes())
|
||||
base_model.write_bytes(b"exact base model bytes")
|
||||
training_args.write_text("epochs: 30\nseed: 0\n", encoding="utf-8")
|
||||
training_results.write_text("epoch,metric\n1,0.1\n", encoding="utf-8")
|
||||
dataset_yaml.write_text("names:\n 0: building\n", encoding="utf-8")
|
||||
dataset_summary.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok",
|
||||
"class_names": ["building"],
|
||||
"tile_count": 198,
|
||||
"train_tile_count": 180,
|
||||
"val_tile_count": 18,
|
||||
"label_count": 58_820,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
training_summary.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok",
|
||||
"trained_model_sha256": _sha(model.read_bytes()),
|
||||
"base_model_sha256": _sha(base_model.read_bytes()),
|
||||
"dataset_summary_sha256": _sha(dataset_summary.read_bytes()),
|
||||
"dataset_yaml_sha256": _sha(dataset_yaml.read_bytes()),
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return module.parse_args(
|
||||
[
|
||||
"--model-path",
|
||||
str(model),
|
||||
"--checkpoint-path",
|
||||
str(checkpoint),
|
||||
"--base-model-path",
|
||||
str(base_model),
|
||||
"--training-summary-path",
|
||||
str(training_summary),
|
||||
"--training-args-path",
|
||||
str(training_args),
|
||||
"--training-results-path",
|
||||
str(training_results),
|
||||
"--dataset-summary-path",
|
||||
str(dataset_summary),
|
||||
"--dataset-yaml-path",
|
||||
str(dataset_yaml),
|
||||
"--source-version",
|
||||
"sprint174-smallbld-minpx3-img640-ft30",
|
||||
"--framework-version",
|
||||
"8.4.93",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_recovered_evidence_requires_byte_identical_checkpoint_and_recorded_hashes(tmp_path: Path) -> None:
|
||||
args = _args(tmp_path)
|
||||
|
||||
evidence = module.inspect_evidence(args)
|
||||
|
||||
assert evidence["checksums"]["model"] == evidence["checksums"]["checkpoint"]
|
||||
assert evidence["checksums"]["training_summary"] == _sha(
|
||||
Path(args.training_summary_path).read_bytes()
|
||||
)
|
||||
assert evidence["class_mapping"] == {"0": "building"}
|
||||
|
||||
|
||||
def test_recovered_evidence_rejects_changed_checkpoint(tmp_path: Path) -> None:
|
||||
args = _args(tmp_path)
|
||||
Path(args.checkpoint_path).write_bytes(b"other checkpoint")
|
||||
|
||||
exit_code, payload = module.migrate(args)
|
||||
|
||||
assert exit_code == 2
|
||||
assert payload["status"] == "evidence_invalid"
|
||||
assert "checkpoint/model SHA-256 mismatch" in payload["message"]
|
||||
|
||||
|
||||
def test_generated_sidecar_passes_exact_runtime_contract(tmp_path: Path) -> None:
|
||||
args = _args(tmp_path)
|
||||
evidence = module.inspect_evidence(args)
|
||||
payload = module._manifest_payload(
|
||||
args=args,
|
||||
evidence=evidence,
|
||||
source_registry_id=str(uuid4()),
|
||||
source_snapshot_id=str(uuid4()),
|
||||
imported_at="2026-08-23T21:00:00+00:00",
|
||||
)
|
||||
manifest_path = RuntimeModelProvenanceService.manifest_path_for_model(args.model_path)
|
||||
assert module._write_manifest_atomically(manifest_path, payload) is True
|
||||
|
||||
validated = RuntimeModelProvenanceService.validate_for_runtime(
|
||||
model_path=args.model_path,
|
||||
model_id="yolo-configured",
|
||||
task_type="object_detection",
|
||||
expected_model_version="sprint174-smallbld-minpx3-img640-ft30",
|
||||
allowed_frameworks=("ultralytics/pytorch",),
|
||||
)
|
||||
|
||||
assert validated.model_sha256 == evidence["checksums"]["model"]
|
||||
assert validated.runtime_manifest_sha256 == payload["metadata"]["runtime_manifest_sha256"]
|
||||
assert module._write_manifest_atomically(manifest_path, payload) is False
|
||||
|
||||
|
||||
def test_model_registry_definition_is_runtime_ready_without_mutating_server_owned_row() -> None:
|
||||
from app.services.source_registry_service import SourceRegistryService
|
||||
|
||||
definition = SourceRegistryService.definition_for("model")
|
||||
|
||||
assert definition.ingest_status == "configured"
|
||||
assert definition.freshness_status == "current"
|
||||
script = SCRIPT.read_text(encoding="utf-8")
|
||||
assert 'source.ingest_status = "configured"' not in script
|
||||
assert 'source.freshness_status = "current"' not in script
|
||||
|
||||
|
||||
def test_model_registry_status_migration_is_narrow_and_restores_write_guard() -> None:
|
||||
migration = (
|
||||
ROOT
|
||||
/ "backend"
|
||||
/ "alembic"
|
||||
/ "versions"
|
||||
/ "202608230001_configure_model_source_registry.py"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert "WHERE source_key = 'model'" in migration
|
||||
assert "registry_owner' = 'server'" in migration
|
||||
assert migration.count("DISABLE TRIGGER trg_source_registry_write_guard") == 1
|
||||
assert migration.count("ENABLE TRIGGER trg_source_registry_write_guard") == 1
|
||||
assert 'down_revision = "202608010001"' in migration
|
||||
|
||||
|
||||
def test_recovery_receipt_requires_hashed_backup_inventory(tmp_path: Path) -> None:
|
||||
args = _args(tmp_path)
|
||||
evidence = module.inspect_evidence(args)
|
||||
receipt = tmp_path / "dry-run.json"
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ready_to_apply",
|
||||
"claim_boundary": module.CLAIM_BOUNDARY,
|
||||
"evidence": evidence,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
storage_manifest = tmp_path / "storage-manifest.tsv"
|
||||
missing_names = ("checkpoint", "training_summary", "training_args", "training_results")
|
||||
rows = ["relative_path\tsize_bytes\tmtime_ns\tsha256"]
|
||||
for name in missing_names:
|
||||
original = Path(evidence["paths"][name])
|
||||
evidence["paths"][name] = f"/app/storage/training/{original.name}"
|
||||
rows.append(f"training/{original.name}\t1\t0\t{evidence['checksums'][name]}")
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ready_to_apply",
|
||||
"claim_boundary": module.CLAIM_BOUNDARY,
|
||||
"evidence": evidence,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
storage_manifest.write_text("\n".join(rows) + "\n", encoding="utf-8")
|
||||
backup_checksums = tmp_path / "CHECKSUMS.sha256"
|
||||
backup_checksums.write_text(
|
||||
f"{_sha(storage_manifest.read_bytes())} storage-manifest.tsv\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
args.evidence_receipt_path = str(receipt)
|
||||
args.backup_storage_manifest_path = str(storage_manifest)
|
||||
args.backup_checksums_path = str(backup_checksums)
|
||||
|
||||
recovered = module.inspect_evidence(args)
|
||||
|
||||
assert recovered["checksums"] == evidence["checksums"]
|
||||
assert recovered["recovery_receipt"]["missing_artifacts_not_recreated"] == list(missing_names)
|
||||
|
||||
storage_manifest.write_text(storage_manifest.read_text(encoding="utf-8") + "tampered\n", encoding="utf-8")
|
||||
exit_code, payload = module.migrate(args)
|
||||
assert exit_code == 2
|
||||
assert "backup storage manifest SHA-256 mismatch" in payload["message"]
|
||||
Reference in New Issue
Block a user