| """ |
| 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 |
|
|
| |
|
|
| 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(): |
| |
| 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 |
|
|
|
|
| |
|
|
| 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 |
| |
| 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} |
| 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"] |
| 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, |
| ) |
|
|
|
|
| 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 |
|
|
|
|
| |
|
|
| 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" |
| |
| |
| |
| dumped = vc.to_dict() |
| assert VerticalConfig.from_dict(dumped).to_dict() == dumped |
|
|