feat(s51): fail-fast self-contained routing config loader
Gate: pytest tests/test_hitl.py -k routing_config → 6 passed.
This commit is contained in:
parent
b50e3fdff2
commit
e9179271e6
2 changed files with 121 additions and 0 deletions
|
|
@ -15,6 +15,9 @@ from __future__ import annotations
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
|
||||
from portfolio_optimiser.outbox import write_outbox
|
||||
from portfolio_optimiser.provenance import Citation, ProvenanceStamp
|
||||
|
|
@ -138,3 +141,75 @@ def test_hitl_registered_maf_free() -> None:
|
|||
from tests.test_okf import _MAF_FREE_MODULES
|
||||
|
||||
assert "hitl.py" in _MAF_FREE_MODULES
|
||||
|
||||
|
||||
# --- load_routing_config() fail-fast ---------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_config(tmp_path: Path, data: dict) -> Path:
|
||||
p = tmp_path / "routing-config.json"
|
||||
p.write_text(json.dumps(data), encoding="utf-8")
|
||||
return p
|
||||
|
||||
|
||||
_WELLFORMED_CONFIG = {
|
||||
"_note": "ignored underscore key",
|
||||
"entries": [
|
||||
{
|
||||
"id": "energi",
|
||||
"allowed_measure_types": ["scope_reduction"],
|
||||
"allowed_code_prefixes": ["05"],
|
||||
"expert": "Ola Energi",
|
||||
},
|
||||
{"id": "vei", "allowed_code_prefixes": ["03"], "expert": "Kari Vei"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_routing_config_loads_wellformed(tmp_path: Path) -> None:
|
||||
"""A well-formed config loads: entries + experts present, ``_``-prefixed top-level keys stripped."""
|
||||
config = hitl.load_routing_config(_write_config(tmp_path, _WELLFORMED_CONFIG))
|
||||
assert [e.id for e in config.entries] == ["energi", "vei"]
|
||||
assert {e.expert for e in config.entries} == {"Ola Energi", "Kari Vei"}
|
||||
assert config.entries[1].allowed_measure_types == frozenset() # empty default = "any measure"
|
||||
|
||||
|
||||
def test_routing_config_missing_expert_raises(tmp_path: Path) -> None:
|
||||
"""An entry missing the required ``expert`` field fails validation."""
|
||||
bad = {"entries": [{"id": "energi", "allowed_code_prefixes": ["05"]}]}
|
||||
with pytest.raises(ValidationError):
|
||||
hitl.load_routing_config(_write_config(tmp_path, bad))
|
||||
|
||||
|
||||
def test_routing_config_duplicate_id_raises(tmp_path: Path) -> None:
|
||||
"""Two entries with the same ``id`` raise (ValueError via the after-validator)."""
|
||||
dup = {
|
||||
"entries": [
|
||||
{"id": "energi", "expert": "A"},
|
||||
{"id": "energi", "expert": "B"},
|
||||
]
|
||||
}
|
||||
with pytest.raises(ValueError):
|
||||
hitl.load_routing_config(_write_config(tmp_path, dup))
|
||||
|
||||
|
||||
def test_routing_config_wrong_types_raise(tmp_path: Path) -> None:
|
||||
"""A non-list ``entries`` (wrong shape) raises."""
|
||||
with pytest.raises(ValidationError):
|
||||
hitl.load_routing_config(_write_config(tmp_path, {"entries": {"id": "x", "expert": "y"}}))
|
||||
|
||||
|
||||
def test_routing_config_missing_file_raises(tmp_path: Path) -> None:
|
||||
"""A missing config file fails fast with ``FileNotFoundError`` (mirrors ``load_pricing``)."""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
hitl.load_routing_config(tmp_path / "no-such-routing-config.json")
|
||||
|
||||
|
||||
def test_routing_config_valid_variant_loads(tmp_path: Path) -> None:
|
||||
"""Control: a minimal valid config (single entry, no code/measure constraints) loads — proving
|
||||
the raises above fire only on the bad case, not always."""
|
||||
config = hitl.load_routing_config(
|
||||
_write_config(tmp_path, {"entries": [{"id": "generalist", "expert": "Per"}]})
|
||||
)
|
||||
assert config.entries[0].expert == "Per"
|
||||
assert config.entries[0].allowed_code_prefixes == frozenset()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue