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
224 lines
7.7 KiB
Python
224 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = ROOT / "scripts" / "activate_promoted_yolo_candidate.py"
|
|
CANDIDATE_KEY = "geointel-building-yolov8s-aoi1024bg512r3e50-pt|512|64|0.35"
|
|
|
|
|
|
def _write_model(models_dir: Path) -> Path:
|
|
model_file = models_dir / "geointel-building-yolov8s-aoi1024bg512r3e50.pt"
|
|
model_file.parent.mkdir(parents=True)
|
|
model_file.write_bytes(b"local promoted model")
|
|
return model_file
|
|
|
|
|
|
def _write_report(path: Path, *, promotion_status: str = "promote_candidate") -> None:
|
|
rejection_reasons = [] if promotion_status == "promote_candidate" else ["background_false_positive_pressure"]
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"gates": {
|
|
"max_background_detections_per_sample": 0,
|
|
"min_background_samples": 2,
|
|
"min_mean_f1": 0.25,
|
|
"min_positive_samples": 7,
|
|
},
|
|
"recommended_candidate": {
|
|
"background_sample_count": 3,
|
|
"background_samples": [["arendonk_heide", 0], ["lommel_heide", 0], ["postel_bos", 0]],
|
|
"candidate_key": CANDIDATE_KEY,
|
|
"max_background_detections": 0,
|
|
"mean_f1": 0.32086574003576274,
|
|
"mean_precision": 0.8400057773951873,
|
|
"mean_recall": 0.20213514285308795,
|
|
"model_asset_id": "geointel-building-yolov8s-aoi1024bg512r3e50-pt",
|
|
"positive_sample_count": 7,
|
|
"promotion_status": promotion_status,
|
|
"rejection_reasons": rejection_reasons,
|
|
"threshold": 0.35,
|
|
"tile_overlap": 64,
|
|
"tile_size": 512,
|
|
"total_background_detections": 0,
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _write_governed_release_gate(path: Path, model_file: Path, **overrides: object) -> None:
|
|
payload: dict[str, object] = {
|
|
"status": "pass",
|
|
"product_benchmark_status": "pass",
|
|
"promotion_allowed": True,
|
|
"phase_decision": "ready",
|
|
"candidate_key": CANDIDATE_KEY,
|
|
"candidate_model_sha256": hashlib.sha256(model_file.read_bytes()).hexdigest(),
|
|
"benchmark_manifest_sha256": "b" * 64,
|
|
}
|
|
payload.update(overrides)
|
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
|
|
|
|
|
def _run_activation(tmp_path: Path, *extra_args: str) -> subprocess.CompletedProcess[str]:
|
|
models_dir = tmp_path / "models"
|
|
model_file = _write_model(models_dir)
|
|
report_path = tmp_path / "promotion_report.json"
|
|
_write_report(report_path)
|
|
release_gate_path = tmp_path / "phase4-release-gate.json"
|
|
_write_governed_release_gate(release_gate_path, model_file)
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("GEOINTEL_ENV=production\nYOLO_ENABLED=false\n", encoding="utf-8")
|
|
|
|
return subprocess.run(
|
|
[
|
|
"python",
|
|
str(SCRIPT),
|
|
"--promotion-report",
|
|
str(report_path),
|
|
"--candidate-key",
|
|
CANDIDATE_KEY,
|
|
"--phase4-release-gate-report",
|
|
str(release_gate_path),
|
|
"--models-dir",
|
|
str(models_dir),
|
|
"--container-model-dir",
|
|
"/app/models",
|
|
"--env-file",
|
|
str(env_file),
|
|
"--json",
|
|
*extra_args,
|
|
],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def test_promoted_yolo_activation_dry_run_validates_report_and_model(tmp_path: Path) -> None:
|
|
result = _run_activation(tmp_path)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
payload = json.loads(result.stdout)
|
|
assert payload["status"] == "ready_to_apply"
|
|
assert payload["applied"] is False
|
|
assert payload["will_download_models"] is False
|
|
assert payload["candidate"]["candidate_key"] == CANDIDATE_KEY
|
|
assert payload["candidate"]["threshold"] == 0.35
|
|
assert payload["selected_model_sha256"] == hashlib.sha256(
|
|
(tmp_path / "models" / "geointel-building-yolov8s-aoi1024bg512r3e50.pt").read_bytes()
|
|
).hexdigest()
|
|
assert payload["env_updates"]["YOLO_ENABLED"] == "true"
|
|
assert payload["env_updates"]["YOLO_MODEL_PATH"] == "/app/models/geointel-building-yolov8s-aoi1024bg512r3e50.pt"
|
|
|
|
|
|
def test_promoted_yolo_activation_apply_updates_env_file(tmp_path: Path) -> None:
|
|
result = _run_activation(tmp_path, "--apply")
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
payload = json.loads(result.stdout)
|
|
assert payload["status"] == "applied"
|
|
env_text = (tmp_path / ".env").read_text(encoding="utf-8")
|
|
assert "GEOINTEL_ENV=production" in env_text
|
|
assert "GEOINTEL_INSTALL_AI=true" in env_text
|
|
assert "YOLO_ENABLED=true" in env_text
|
|
assert "YOLO_MODELS_DIR=/app/models" in env_text
|
|
assert "YOLO_MODEL_PATH=/app/models/geointel-building-yolov8s-aoi1024bg512r3e50.pt" in env_text
|
|
|
|
|
|
def test_promoted_yolo_activation_rejects_non_promoted_report(tmp_path: Path) -> None:
|
|
models_dir = tmp_path / "models"
|
|
model_file = _write_model(models_dir)
|
|
report_path = tmp_path / "promotion_report.json"
|
|
_write_report(report_path, promotion_status="reject")
|
|
release_gate_path = tmp_path / "phase4-release-gate.json"
|
|
_write_governed_release_gate(release_gate_path, model_file)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"python",
|
|
str(SCRIPT),
|
|
"--promotion-report",
|
|
str(report_path),
|
|
"--candidate-key",
|
|
CANDIDATE_KEY,
|
|
"--phase4-release-gate-report",
|
|
str(release_gate_path),
|
|
"--models-dir",
|
|
str(models_dir),
|
|
"--env-file",
|
|
str(tmp_path / ".env"),
|
|
"--json",
|
|
],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode == 3
|
|
payload = json.loads(result.stdout)
|
|
assert payload["status"] == "candidate_not_promoted"
|
|
assert "rejection_reasons" in payload
|
|
|
|
|
|
def test_promoted_yolo_activation_rejects_failed_or_unbound_phase4_gate(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
models_dir = tmp_path / "models"
|
|
model_file = _write_model(models_dir)
|
|
report_path = tmp_path / "promotion_report.json"
|
|
_write_report(report_path)
|
|
release_gate_path = tmp_path / "phase4-release-gate.json"
|
|
_write_governed_release_gate(
|
|
release_gate_path,
|
|
model_file,
|
|
status="fail",
|
|
promotion_allowed=False,
|
|
candidate_model_sha256="0" * 64,
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"python",
|
|
str(SCRIPT),
|
|
"--promotion-report",
|
|
str(report_path),
|
|
"--phase4-release-gate-report",
|
|
str(release_gate_path),
|
|
"--candidate-key",
|
|
CANDIDATE_KEY,
|
|
"--models-dir",
|
|
str(models_dir),
|
|
"--env-file",
|
|
str(tmp_path / ".env"),
|
|
"--json",
|
|
],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode == 3
|
|
payload = json.loads(result.stdout)
|
|
assert payload["status"] == "governed_release_not_approved"
|
|
assert "phase4_release_gate_not_passed" in payload["rejection_reasons"]
|
|
assert "governed_candidate_model_sha256_mismatch" in payload["rejection_reasons"]
|
|
|
|
|
|
def test_readiness_gate_compiles_promoted_activation_script() -> None:
|
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
|
|
|
assert "-m py_compile scripts/activate_promoted_yolo_candidate.py" in readiness
|