from __future__ import annotations import json import subprocess import sys from pathlib import Path from app.core.config import Settings from app.services.yolo_preflight_service import YoloPreflightService ROOT = Path(__file__).resolve().parents[2] class AvailableAdapter: @staticmethod def dependencies_available() -> bool: return True def __init__(self, settings: Settings) -> None: self.settings = settings def load_model(self, model_path: Path) -> object: return {"model_path": str(model_path)} class MissingDependencyAdapter: @staticmethod def dependencies_available() -> bool: return False class FailingLoadAdapter(AvailableAdapter): def load_model(self, model_path: Path) -> object: raise RuntimeError(f"cannot load {model_path}") def _manifest(tmp_path: Path, tile_count: int = 1) -> Path: tiles = [] for index in range(tile_count): tile_path = tmp_path / f"tile_{index:04d}.tif" tile_path.write_bytes(b"tile") tiles.append( { "path": str(tile_path), "pixel_window": [0, 0, 100, 100], "bounds": [4.0, 51.0, 5.0, 52.0], "transform": [4.0, 0.01, 0.0, 52.0, 0.0, -0.01], "index": index, } ) manifest_path = tmp_path / "manifest.json" manifest_path.write_text(json.dumps({"tiles": tiles, "count": tile_count}), encoding="utf-8") return manifest_path def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path) -> None: result = YoloPreflightService.run( settings=Settings(yolo_enabled=False, yolo_model_path=str(tmp_path / "missing.pt")), tile_manifest_path=str(tmp_path / "missing-manifest.json"), yolo_adapter_class=AvailableAdapter, ) assert result["status"] == "not_configured" assert result["checks"]["enabled"] is False assert result["checks"]["dependencies_available"] is None assert result["checks"]["model_file_exists"] is None assert result["checks"]["manifest_valid"] is None def test_yolo_preflight_distinguishes_missing_dependencies_from_missing_model(tmp_path: Path) -> None: model_path = tmp_path / "model.pt" model_path.write_bytes(b"weights") result = YoloPreflightService.run( settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path)), tile_manifest_path=str(_manifest(tmp_path)), yolo_adapter_class=MissingDependencyAdapter, ) assert result["status"] == "dependency_unavailable" assert result["checks"]["dependencies_available"] is False assert result["checks"]["model_file_exists"] is None assert result["checks"]["manifest_valid"] is None def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_path: Path) -> None: model_path = tmp_path / "model.pt" model_path.write_bytes(b"weights") manifest_path = _manifest(tmp_path, tile_count=2) result = YoloPreflightService.run( settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4), tile_manifest_path=str(manifest_path), yolo_adapter_class=AvailableAdapter, ) assert result["status"] == "ready" assert result["checks"]["dependencies_available"] is True assert result["checks"]["model_file_exists"] is True assert result["checks"]["manifest_valid"] is True assert result["tile_count"] == 2 assert result["will_download_models"] is False assert result["will_run_inference"] is False def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) -> None: model_path = tmp_path / "model.pt" model_path.write_bytes(b"weights") manifest_path = _manifest(tmp_path) result = YoloPreflightService.run( settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4), tile_manifest_path=str(manifest_path), yolo_adapter_class=AvailableAdapter, check_model_load=True, ) assert result["status"] == "ready" assert result["checks"]["model_load_requested"] is True assert result["checks"]["model_load_ok"] is True assert result["will_download_models"] is False assert result["will_run_inference"] is False def test_yolo_preflight_reports_explicit_model_load_failure(tmp_path: Path) -> None: model_path = tmp_path / "model.pt" model_path.write_bytes(b"weights") result = YoloPreflightService.run( settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4), tile_manifest_path=str(_manifest(tmp_path)), yolo_adapter_class=FailingLoadAdapter, check_model_load=True, ) assert result["status"] == "model_load_failed" assert result["error_code"] == "DETECTION_MODEL_LOAD_FAILED" assert result["checks"]["model_load_ok"] is False def test_yolo_preflight_script_outputs_json(tmp_path: Path) -> None: model_path = tmp_path / "model.pt" model_path.write_bytes(b"weights") manifest_path = _manifest(tmp_path) result = subprocess.run( [ sys.executable, str(ROOT / "scripts" / "yolo_preflight.py"), "--model-path", str(model_path), "--tile-manifest-path", str(manifest_path), "--assume-dependencies", "--json", ], cwd=ROOT, check=True, capture_output=True, text=True, ) payload = json.loads(result.stdout) assert payload["status"] == "ready" assert payload["model_path"] == str(model_path) assert payload["tile_manifest_path"] == str(manifest_path) def test_yolo_preflight_script_rejects_assumed_dependencies_for_model_load(tmp_path: Path) -> None: result = subprocess.run( [ sys.executable, str(ROOT / "scripts" / "yolo_preflight.py"), "--model-path", str(tmp_path / "model.pt"), "--assume-dependencies", "--check-model-load", "--json", ], cwd=ROOT, check=False, capture_output=True, text=True, ) assert result.returncode != 0 assert "--check-model-load cannot be combined with --assume-dependencies" in result.stderr