New load_dimension(str | Path) in dimension.py: is_file() -> FileNotFoundError, then Dimension.model_validate_json -> pydantic.ValidationError on malformed shape. Fail-fast because dimension config is authoritative startup input (contrast the tolerant verdict-inbox RAW layer). Stdlib + pydantic only — stays MAF-free (test_okf_is_maf_free AST guard green). Exported from __init__.py in both the import block and __all__. RED-first: 3 loader tests failed on the missing import, green after. tests/test_dimension.py + test_okf.py: 29 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNNiJRk1sSwxgVLS5AobT1
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""Unit tests for the dimension IR + ``admits`` scoping gate (Fase 1, Step 1, SC1).
|
|
|
|
``admits`` must accept an in-dimension candidate (``measure_type`` in the allowed
|
|
set) and reject an out-of-dimension one, exercising BOTH branches of the
|
|
code-prefix logic — empty prefixes (measure_type-only) and non-empty prefixes
|
|
where a matching vs non-matching code decides the outcome. A ``Dimension`` with an
|
|
empty ``label`` must fail construction (Pydantic ``ValidationError``).
|
|
|
|
Model test: ``tests/test_validator.py`` (rejection assertions).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from portfolio_optimiser.dimension import Dimension, admits, load_dimension
|
|
|
|
|
|
def _energy_dim(*, prefixes: frozenset[str] = frozenset()) -> Dimension:
|
|
return Dimension(
|
|
id="energi",
|
|
label="Energi",
|
|
allowed_measure_types=frozenset({"energy_efficiency"}),
|
|
allowed_code_prefixes=prefixes,
|
|
)
|
|
|
|
|
|
def test_admits_in_dimension_measure_type_only() -> None:
|
|
"""Empty ``allowed_code_prefixes``: only the measure_type gate applies."""
|
|
dim = _energy_dim()
|
|
assert (
|
|
admits(measure_type="energy_efficiency", codes=frozenset({"07.1"}), dimension=dim) is True
|
|
)
|
|
|
|
|
|
def test_admits_rejects_out_of_dimension_measure_type() -> None:
|
|
"""Out-of-dimension measure_type is rejected regardless of codes."""
|
|
dim = _energy_dim()
|
|
assert admits(measure_type="paving", codes=frozenset({"07.1"}), dimension=dim) is False
|
|
|
|
|
|
def test_admits_prefix_branch_matching_code() -> None:
|
|
"""Non-empty prefixes: a code matching a prefix is admitted."""
|
|
dim = _energy_dim(prefixes=frozenset({"07"}))
|
|
assert (
|
|
admits(
|
|
measure_type="energy_efficiency",
|
|
codes=frozenset({"07.1", "99.9"}),
|
|
dimension=dim,
|
|
)
|
|
is True
|
|
)
|
|
|
|
|
|
def test_admits_prefix_branch_no_matching_code() -> None:
|
|
"""Non-empty prefixes: no code matches any prefix -> rejected even when measure_type is allowed."""
|
|
dim = _energy_dim(prefixes=frozenset({"07"}))
|
|
assert (
|
|
admits(
|
|
measure_type="energy_efficiency",
|
|
codes=frozenset({"99.9"}),
|
|
dimension=dim,
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
def test_dimension_empty_label_raises() -> None:
|
|
"""An empty ``label`` violates ``Field(min_length=1)`` -> ValidationError at construction."""
|
|
with pytest.raises(ValidationError):
|
|
Dimension(
|
|
id="energi",
|
|
label="",
|
|
allowed_measure_types=frozenset({"energy_efficiency"}),
|
|
)
|
|
|
|
|
|
# --- Step 1 (S5.3): fail-fast load_dimension loader (mirrors load_goal_config) -------------------
|
|
|
|
|
|
def test_load_dimension_round_trip(tmp_path) -> None:
|
|
"""A valid dimension JSON round-trips through ``load_dimension`` (accepts str | Path)."""
|
|
dim = Dimension(
|
|
id="energi",
|
|
label="Energi",
|
|
allowed_measure_types=frozenset({"energy_efficiency"}),
|
|
allowed_code_prefixes=frozenset({"07"}),
|
|
)
|
|
p = tmp_path / "dim.json"
|
|
p.write_text(dim.model_dump_json(), encoding="utf-8")
|
|
assert load_dimension(p) == dim
|
|
assert load_dimension(str(p)) == dim # str path also accepted
|
|
|
|
|
|
def test_load_dimension_missing_file_raises(tmp_path) -> None:
|
|
"""A missing file fails fast with ``FileNotFoundError`` — authoritative startup config,
|
|
NOT a tolerant RAW inbox layer (contrast the verdict inbox)."""
|
|
with pytest.raises(FileNotFoundError):
|
|
load_dimension(tmp_path / "does-not-exist.json")
|
|
|
|
|
|
def test_load_dimension_malformed_shape_raises(tmp_path) -> None:
|
|
"""Malformed content (missing required fields) fails fast with ``ValidationError``."""
|
|
bad = tmp_path / "dim.json"
|
|
bad.write_text('{"id": "energi"}', encoding="utf-8") # missing label + allowed_measure_types
|
|
with pytest.raises(ValidationError):
|
|
load_dimension(bad)
|