"""The shipped YAML carries thresholds measured on the deployment site; the pydantic defaults must not drift away from them (a trimmed config would then silently re-admit the false positives those values were tuned to reject).""" from pathlib import Path import pytest from behavision.config import (ApiSection, Config, DetectionSection, RecognitionSection, TrackingSection, ensure_api_credentials, load_config) def test_yaml_matches_code_defaults(): cfg = load_config() for section, model in (("detection", DetectionSection), ("recognition", RecognitionSection), ("tracking", TrackingSection)): loaded = getattr(cfg, section) defaults = model() for name in model.model_fields: assert getattr(loaded, name) == getattr(defaults, name), ( f"{section}.{name} drifted between config/default.yaml " f"and config.py") def test_tuned_thresholds_are_the_defaults(): cfg = Config() assert cfg.detection.score_threshold == 0.82 assert cfg.recognition.min_enroll_quality == 0.65 def test_loopback_stays_open(tmp_path): cfg = Config() cfg.app.data_dir = tmp_path cfg.api.host = "127.0.0.1" auth_on, generated = ensure_api_credentials(cfg) assert (auth_on, generated) == (False, False) assert not (tmp_path / "api_credentials.txt").exists() def test_routable_host_never_served_unauthenticated(tmp_path): cfg = Config() cfg.app.data_dir = tmp_path cfg.api.host = "0.0.0.0" auth_on, generated = ensure_api_credentials(cfg) assert auth_on and generated assert cfg.api.username and len(cfg.api.password) >= 16 assert (tmp_path / "api_credentials.txt").exists() def test_generated_credentials_are_stable_across_restarts(tmp_path): first = Config() first.app.data_dir = tmp_path first.api.host = "0.0.0.0" ensure_api_credentials(first) second = Config() second.app.data_dir = tmp_path second.api.host = "0.0.0.0" auth_on, generated = ensure_api_credentials(second) assert auth_on and not generated # reused, not re-minted assert second.api.password == first.api.password def test_configured_credentials_win(tmp_path): cfg = Config() cfg.app.data_dir = tmp_path cfg.api.host = "0.0.0.0" cfg.api.username, cfg.api.password = "alice", "s3cret" auth_on, generated = ensure_api_credentials(cfg) assert (auth_on, generated) == (True, False) assert not (tmp_path / "api_credentials.txt").exists() assert cfg.api.password == "s3cret" def test_blank_env_placeholders_do_not_enable_auth(): """Unset ${ENV} parses as YAML null; auth must not half-enable.""" cfg = ApiSection(host="0.0.0.0", port=None, username=None, password=None) assert cfg.username == "" and cfg.password == "" assert cfg.port == 8010 assert not cfg.auth_enabled def test_color_order_is_declared_per_model_family(): """ArcFace/InsightFace exports want RGB, AdaFace wants BGR. Same scaling, opposite channel order, and feeding the wrong one raises nothing — it just yields off-distribution embeddings. Pin the mapping down.""" from behavision.recognition import color_order_for for name in ("w600k_mbf", "w600k_r50", "arcface", "arcface_int8"): assert color_order_for(name) == "RGB", name for name in ("adaface_ir18", "adaface_ir50", "adaface_ir101", "AdaFace_IR50"): assert color_order_for(name) == "BGR", name def test_better_models_are_preferred_over_the_fallback(): from behavision.recognition import MODEL_CANDIDATES order = {n: i for i, n in enumerate(MODEL_CANDIDATES)} # first that loads wins, so accuracy must come before the 13 MB fallback assert order["w600k_r50.onnx"] < order["w600k_mbf.onnx"] assert order["adaface_ir50.onnx"] < order["w600k_r50.onnx"] def test_unloadable_model_falls_through_to_the_next_candidate(tmp_path): """The deploy box may not have RAM for the big model. A corrupt/unusable file must be skipped, not fatal — otherwise a memory-starved machine can no longer start at all.""" import shutil from pathlib import Path from behavision.recognition import ArcFaceEncoder real = Path(__file__).resolve().parent.parent / "models" / "w600k_mbf.onnx" if not real.exists(): import pytest pytest.skip("no recognition model installed") # a plausible-looking but unloadable "big model" ahead of a good one (tmp_path / "w600k_r50.onnx").write_bytes(b"not an onnx file") shutil.copy(real, tmp_path / "w600k_mbf.onnx") enc = ArcFaceEncoder(tmp_path) assert enc.model_name == "w600k_mbf" # fell through, did not crash def test_no_usable_model_fails_loudly(tmp_path): from behavision.recognition import ArcFaceEncoder (tmp_path / "w600k_mbf.onnx").write_bytes(b"garbage") with pytest.raises(FileNotFoundError, match="no usable recognition model"): ArcFaceEncoder(tmp_path) def _no_env(monkeypatch, tmp_path): """Make the process look like an installed PC rather than a checkout. `load_config` calls `load_dotenv` first, so simply unsetting the variables is not enough in a working tree - the .env sitting next to the code puts them straight back. Removing the .env from the picture is the condition these tests are actually about. """ import behavision.paths as paths monkeypatch.setattr(paths, "env_file", lambda: None) for var in ("BEHAVISION_CAM1_HOST", "BEHAVISION_CAM1_USERNAME", "BEHAVISION_CAM1_PASSWORD", "BEHAVISION_API_USER", "BEHAVISION_API_PASSWORD", "BEHAVISION_WEBHOOK_URL"): monkeypatch.delenv(var, raising=False) monkeypatch.setenv("BEHAVISION_DATA_DIR", str(tmp_path)) def test_the_bundled_config_loads_with_no_environment_at_all(tmp_path, monkeypatch): """What a freshly installed PC looks like: config/default.yaml, and nothing else. Every `${VAR}` in that file then resolves to YAML null. The engine's own sections already guarded against that; the camera section did not, so the shipped config raised three pydantic errors on host, username and password and the engine would not start at all on any machine without a developer's .env - which is every machine the product is installed on. It was found by cloning the repository and running this suite, never by working in a checkout where .env is right there. """ _no_env(monkeypatch, tmp_path) root = Path(__file__).resolve().parent.parent cfg = load_config(root / "config" / "default.yaml") assert cfg.api.username == "" assert cfg.api.port == 8010 for cam in cfg.cameras: assert cam.host == "" and cam.username == "" and cam.password == "" assert cam.port == 554 def test_a_camera_with_no_address_is_not_seeded(tmp_path, monkeypatch): """The bundled config declares a camera whose address comes from the environment. On an installed PC there is none, so seeding it would put a camera the shop never added into a brand new install, permanently failing to connect to "" - and their first task would be working out what it was.""" from behavision.cameras import CameraStore _no_env(monkeypatch, tmp_path) root = Path(__file__).resolve().parent.parent cfg = load_config(root / "config" / "default.yaml") store = CameraStore(tmp_path / "cameras.json") assert store.seed(cfg.cameras) is False assert store.list() == [] assert not (tmp_path / "cameras.json").exists()