114 lines
3.6 KiB
Python
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" |