Files
6krrt/tests/test_config_watchdog.py

114 lines
3.6 KiB
Python

"""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"