"""Tests for the synthetic reference domain (D4).""" import pytest from portfolio_optimiser.reference_domain import CostItem, Project, load_reference_projects @pytest.fixture(scope="module") def projects() -> tuple[Project, ...]: return load_reference_projects() def test_loads_non_empty(projects: tuple[Project, ...]) -> None: assert len(projects) >= 1 def test_project_ids_unique(projects: tuple[Project, ...]) -> None: ids = [p.id for p in projects] assert len(ids) == len(set(ids)) def test_every_project_well_formed(projects: tuple[Project, ...]) -> None: for p in projects: assert p.id and p.name and p.description assert p.currency == "NOK" assert len(p.cost_items) >= 1 def test_every_project_has_docs_dir_and_verdict_input(projects: tuple[Project, ...]) -> None: """Step 2 (SC1 prep): docs_dir + verdict_input are config-driven per project. Each loaded Project exposes a docs_dir resolved to an existing bundled directory, and a verdict_input dict with exactly the decision + rationale keys. Removing the loader fields reddens this.""" import os for p in projects: assert p.docs_dir and os.path.isdir(p.docs_dir) assert set(p.verdict_input) == {"decision", "rationale"} assert p.verdict_input["decision"] and p.verdict_input["rationale"] def test_repo_local_mini_bundle_loads_as_ir_projection() -> None: """Step 1 (S2.0 fixture): the repo-local mini OKF-bundle under the package ``data/`` root loads as a valid IR projection via ``okf.load_ir_projection`` (fail-fast), exposing its ``project_id``, and lives under ``data/`` — NEVER under ``shared/`` (the subtree is PULL-ONLY). It is also a navigable OKF bundle (readable ``index.md``), so the Step-3 bundle-context navigation works.""" import os from importlib.resources import files from portfolio_optimiser import okf bundle_dir = str(files("portfolio_optimiser").joinpath("data/bundles/bygg-energi-mikro-a")) assert os.path.isdir(bundle_dir) assert f"{os.sep}data{os.sep}bundles{os.sep}" in bundle_dir assert f"{os.sep}shared{os.sep}" not in bundle_dir ir = okf.load_ir_projection(bundle_dir) assert ir["project_id"] == "BYGG-ENERGI-MIKRO-A" bundle = okf.navigate_bundle(bundle_dir) assert bundle.index_summary.strip() def test_existing_projects_have_null_bundle_and_verdict_dir(projects: tuple[Project, ...]) -> None: """Step 2 (S2.0 schema): the shipped reference_projects.json is UNCHANGED this bolk, so every existing row loads with ``bundle_dir is None`` AND ``verdict_dir is None`` — no ``KeyError`` (the loader reads both via ``p.get(...)``, backward-compatible). Removing either field reddens this.""" for p in projects: assert p.bundle_dir is None assert p.verdict_dir is None def test_project_accepts_optional_bundle_and_verdict_dir() -> None: """Step 2 (S2.0 schema): the frozen Project exposes optional ``bundle_dir`` + ``verdict_dir`` fields a caller can set directly — the concrete sources Step 3 threads through run_portfolio.""" p = Project( id="X", name="x", description="d", currency="NOK", cost_items=(), docs_dir="/tmp/x", verdict_input={"decision": "approved", "rationale": "r"}, bundle_dir="/tmp/bundle", verdict_dir="/tmp/inbox", ) assert p.bundle_dir == "/tmp/bundle" assert p.verdict_dir == "/tmp/inbox" def test_cost_item_total_is_quantity_times_unit_cost() -> None: item = CostItem(code="X", description="d", quantity=4.0, unit="m2", unit_cost=215.0) assert item.total_cost == pytest.approx(860.0) def test_project_total_is_sum_of_items(projects: tuple[Project, ...]) -> None: for p in projects: expected = sum(item.total_cost for item in p.cost_items) assert p.total_cost == pytest.approx(expected) assert p.total_cost > 0