The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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="licence", 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)
|