portfolio-optimiser/tests/test_dimension.py
Kjell Tore Guttormsen 37547fe292
refactor(examples): replace sector-specific example material with generic, fictitious examples
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>
2026-09-23 15:04:21 +02:00

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)