from __future__ import annotations import asyncio from concurrent.futures import ThreadPoolExecutor from pathlib import Path from cmvr_edge_ai.application import ( ApplicationState, EdgeAIApplication, create_default_registry, validate_application, ) from cmvr_edge_ai.cli import main from cmvr_edge_ai.config import load_config PROJECT_ROOT = Path(__file__).resolve().parents[2] SMOKE_CONFIG = PROJECT_ROOT / "configs" / "smoke.yaml" DETECTION_CONFIG = PROJECT_ROOT / "detect_server" / "pipeline.yaml" def test_application_runs_the_finite_smoke_pipeline() -> None: async def exercise(): # type: ignore[no-untyped-def] loop = asyncio.get_running_loop() host_executor = ThreadPoolExecutor(max_workers=1) loop.set_default_executor(host_executor) application = EdgeAIApplication( load_config(SMOKE_CONFIG), create_default_registry(discover_entry_points=False), ) try: await application.start() running_health = await application.health() await application.wait() await application.stop() # The application owns only its bounded executor. Stopping it must # not poison an embedding event loop's pre-existing executor. host_executor_result = await asyncio.to_thread(lambda: "still-usable") return application, running_health, host_executor_result finally: # Python 3.12 Runner shuts its default executor down from another # helper thread. Explicit cleanup keeps this regression deterministic # in restricted containers where that helper cannot wake the loop. host_executor.shutdown(wait=True, cancel_futures=True) loop._default_executor = None # type: ignore[attr-defined] application, running_health, host_executor_result = asyncio.run(exercise()) assert running_health["state"] == "running" assert set(running_health["pipelines"]) == {"smoke"} assert application.pipelines == ("smoke",) assert application.state is ApplicationState.STOPPED assert host_executor_result == "still-usable" def test_cli_validates_and_runs_smoke_config( capsys, monkeypatch ) -> None: # type: ignore[no-untyped-def] # ``configure_logging(force=True)`` intentionally owns process logging in # production. Avoid leaking that global CLI side effect into later tests. monkeypatch.setattr("cmvr_edge_ai.cli.configure_logging", lambda *_: None) assert main(["validate", "--config", str(SMOKE_CONFIG)]) == 0 validation_output = capsys.readouterr() assert "configuration is valid; pipelines: smoke" in validation_output.out assert validation_output.err == "" assert main(["run", "--config", str(SMOKE_CONFIG), "--log-level", "WARNING"]) == 0 run_output = capsys.readouterr() assert run_output.err == "" def test_cli_lists_versioned_builtin_plugins(capsys) -> None: # type: ignore[no-untyped-def] assert main(["plugins"]) == 0 output = capsys.readouterr().out assert "core.sequence_source@1\tsource" in output assert "detection.model@1\toperator" in output assert "media.video_decoder.pyav@1\toperator" in output assert "safety.robot_command_gate@1\toperator" in output def test_cli_lists_detection_model_metadata(capsys) -> None: # type: ignore[no-untyped-def] assert main(["models"]) == 0 output = capsys.readouterr().out assert ( "construction-ppe-yolov8@1\tConstruction PPE YOLOv8s\t" "ultralytics-yolo" ) in output assert "No-Helmet" in output assert "No-Vest" in output assert ( "ppe-6classes-yolov8n@1\tPPE Detection YOLOv8n (6 Classes)\t" "ultralytics-yolo" ) in output assert "Gloves,Vest,goggles,helmet,mask,safety_shoe" in output def test_real_detection_config_compiles_without_loading_optional_runtimes() -> None: config = load_config(DETECTION_CONFIG) compiled = validate_application( config, create_default_registry(discover_entry_points=False), ) assert len(compiled) == 1 assert ( config.pipelines["detection"].nodes["platform"].params["failure_mode"] == "log_and_drop" ) assert set(compiled[0].plugin_specs) == { "camera", "decoder", "detector", "repeat_gate", "platform", }