"""Config validation tests for the watchdog / notification / detector sections. Each section has defaults that load correctly when absent, and each has knobs that must be validated at config-load time rather than silently accepted. """ from __future__ import annotations import copy from pathlib import Path import pytest import yaml from config import RouterConfig ROOT = Path(__file__).resolve().parent.parent @pytest.fixture def raw() -> dict: with open(ROOT / "config" / "config.yaml") as fh: return yaml.safe_load(fh) def test_watchdog_config_defaults(raw): """When the watchdog section is absent, all defaults are filled in.""" cfg = copy.deepcopy(raw) cfg.pop("watchdog") loaded = RouterConfig(**cfg) assert loaded.watchdog.enabled is True assert loaded.watchdog.local_llm_enabled is True assert loaded.watchdog.model is None assert loaded.watchdog.read_only_agents == ["explore", "librarian", "oracle"] assert loaded.watchdog.dashboard_base_url == "http://127.0.0.1:8080/admin" # Detector sub-config defaults det = loaded.watchdog.detector assert det.window == 60 assert det.dup_min == 0.25 assert det.top_min == 12 assert det.top_min_ro == 8 assert det.cum_min == 15 assert det.cover_min == 4.0 assert det.min_calls == 40 def test_notifications_channels_default(raw): """When the notifications section is absent, one desktop channel is the default.""" cfg = copy.deepcopy(raw) cfg.pop("notifications") loaded = RouterConfig(**cfg) assert len(loaded.notifications.channels) == 1 ch = loaded.notifications.channels[0] assert ch["name"] == "default" assert ch["type"] == "desktop" assert ch["enabled"] is True assert ch["min_severity"] == "warning" def test_notifications_unknown_type_fails(raw): """An unknown notification channel type is rejected at load time.""" cfg = copy.deepcopy(raw) cfg["notifications"]["channels"].append( {"name": "pager", "type": "pagerduty"} ) with pytest.raises(ValueError, match="pagerduty|Input should be"): RouterConfig(**cfg) def test_detector_thresholds(raw): """Override every detector threshold and confirm parsing.""" cfg = copy.deepcopy(raw) cfg["watchdog"]["detector"] = { "window": 120, "dup_min": 0.5, "top_min": 24, "top_min_ro": 16, "cum_min": 30, "cover_min": 8.0, "min_calls": 80, } loaded = RouterConfig(**cfg) det = loaded.watchdog.detector assert det.window == 120 assert det.dup_min == 0.5 assert det.top_min == 24 assert det.top_min_ro == 16 assert det.cum_min == 30 assert det.cover_min == 8.0 assert det.min_calls == 80 def test_watchdog_model_defaults_to_verification(raw): """When watchdog.model is None, the config stores None and falls through to verification.model at runtime.""" cfg = copy.deepcopy(raw) # Shipped config has watchdog.model: null and verification.model set. assert cfg["watchdog"]["model"] is None assert cfg["verification"]["model"] is not None loaded = RouterConfig(**cfg) assert loaded.watchdog.model is None # The fallback logic lives at runtime, not in the model, so we confirm # the config-level contract: watchdog.model None + verification.model set. assert loaded.verification.model is not None # Also verify that when watchdog.model IS set, it takes that value. cfg["watchdog"]["model"] = "custom-verifier:latest" loaded2 = RouterConfig(**cfg) assert loaded2.watchdog.model == "custom-verifier:latest"