""" Tests for core.config — the per-entity-type configuration schema (Stage 1). """ import pytest from core.config import ( HIGHER_IS_WORSE, LOWER_IS_WORSE, EntityTypeConfig, MetricConfig, VerticalConfig, ) from core.drift import DEFAULT_DRIFT_WEIGHTS, DEFAULT_SEVERITY_THRESHOLDS # --- MetricConfig --------------------------------------------------------- def test_metric_config_accepts_canonical_directions(): assert MetricConfig("backlog", HIGHER_IS_WORSE).direction == HIGHER_IS_WORSE assert MetricConfig("uptime", LOWER_IS_WORSE).direction == LOWER_IS_WORSE def test_metric_config_translates_legacy_direction_aliases(): # the legacy data contracts say higher_is_better / lower_is_better assert MetricConfig("uptime", "higher_is_better").direction == LOWER_IS_WORSE assert MetricConfig("backlog", "lower_is_better").direction == HIGHER_IS_WORSE def test_metric_config_rejects_unknown_direction(): with pytest.raises(ValueError): MetricConfig("x", "sideways") def test_metric_config_rejects_negative_weight(): with pytest.raises(ValueError): MetricConfig("x", HIGHER_IS_WORSE, weight=-0.1) def test_metric_config_round_trips_through_dict(): m = MetricConfig("response_time", HIGHER_IS_WORSE, weight=0.3, feeds_stability=True, latency_target=25.0) restored = MetricConfig.from_dict("response_time", m.to_dict()) assert restored == m # --- EntityTypeConfig ----------------------------------------------------- def _branch_dict(): return { "entity_type": "branch", "metrics": { "complaints": {"direction": "higher_is_worse", "weight": 0.5}, "response_time": {"direction": "higher_is_worse", "weight": 0.5, "latency_target": 25.0, "feeds_stability": True}, }, } def test_entity_type_config_from_dict_applies_defaults(): et = EntityTypeConfig.from_dict(_branch_dict()) assert et.entity_type == "branch" assert et.baseline_window == 14 assert et.baseline_lag == 7 # weights/thresholds default to the documented core defaults assert et.signal_weights == DEFAULT_DRIFT_WEIGHTS assert et.severity_thresholds == DEFAULT_SEVERITY_THRESHOLDS def test_entity_type_config_partial_overrides_are_merged(): d = _branch_dict() d["signal_weights"] = {"delta": 0.4} # override one key only d["severity_thresholds"] = {"high": 0.6} et = EntityTypeConfig.from_dict(d) assert et.signal_weights["delta"] == 0.4 assert et.signal_weights["xi"] == DEFAULT_DRIFT_WEIGHTS["xi"] # untouched assert et.severity_thresholds["high"] == 0.6 assert et.severity_thresholds["critical"] == DEFAULT_SEVERITY_THRESHOLDS["critical"] def test_entity_type_config_rejects_no_metrics(): with pytest.raises(ValueError): EntityTypeConfig(entity_type="empty", metrics=[]) def test_entity_type_config_rejects_bad_baseline_lag(): with pytest.raises(ValueError): EntityTypeConfig( entity_type="branch", metrics=[MetricConfig("x", HIGHER_IS_WORSE)], baseline_window=10, baseline_lag=10, # must be strictly less than window ) def test_entity_type_config_rejects_duplicate_metric_names(): with pytest.raises(ValueError): EntityTypeConfig( entity_type="branch", metrics=[MetricConfig("x", HIGHER_IS_WORSE), MetricConfig("x", LOWER_IS_WORSE)], ) def test_entity_type_config_metric_lookup(): et = EntityTypeConfig.from_dict(_branch_dict()) assert et.metric("complaints").weight == 0.5 assert set(et.metric_names) == {"complaints", "response_time"} with pytest.raises(KeyError): et.metric("does_not_exist") def test_entity_type_config_round_trips_through_dict(): et = EntityTypeConfig.from_dict(_branch_dict()) restored = EntityTypeConfig.from_dict(restored_input := et.to_dict()) assert restored.to_dict() == restored_input # --- VerticalConfig ------------------------------------------------------- def test_vertical_config_round_trips_through_dict(): raw = { "name": "logistics", "entity_types": { "branch": {k: v for k, v in _branch_dict().items() if k != "entity_type"}, }, } vc = VerticalConfig.from_dict(raw) assert vc.name == "logistics" assert vc.entity_type("branch").entity_type == "branch" # to_dict() yields the *normalised* form (defaults filled in), so it won't # equal the sparse input — but it must be a stable fixed point: loading it # back and dumping again changes nothing. dumped = vc.to_dict() assert VerticalConfig.from_dict(dumped).to_dict() == dumped