portfolio-optimiser/tests/test_value_report.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

174 lines
7.1 KiB
Python

"""S5.4 — verdirapport unit tests: roll-up correctness (SC1), exact NOK (SC2), determinism (SC6).
In-process build over a hand-constructed ``SavingsLedger``; local ``_entry(...)`` factory (copied
from ``tests/test_ledger.py:22-37``). The roll-up must call the ledger accessors
(``per_project_total`` / ``portfolio_total`` / ``overlaps``), never re-sum ``.entries`` (double-count
risk on cross-dimension overlaps).
"""
from __future__ import annotations
import json
from portfolio_optimiser.ledger import LedgerEntry, SavingsLedger, stamp
from portfolio_optimiser.value_report import (
ProvenanceLine,
build_value_report,
dump_report_json,
format_report_text,
)
_TS = "2026-07-06T00:00:00Z"
def _entry(
project_id: str,
candidate_identity: str,
amount_ore: int,
*,
dimension: str = "energi",
verdict_id: str = "v1",
) -> LedgerEntry:
return LedgerEntry(
project_id=project_id,
dimension=dimension,
candidate_identity=candidate_identity,
amount_ore=amount_ore,
verdict_id=verdict_id,
provenance=stamp(approver="ekspert", experiment="fase1-sim", timestamp=_TS),
)
def _mixed_ledger() -> SavingsLedger:
""">=2 projects, >=2 dimensions, one cross-dimension overlap (the same candidate ``c-a`` realized
under both ``energi`` and ``lisens`` in P1 -> counted ONCE by the deduped total, FLAGGED)."""
led = SavingsLedger()
led.add_realized(_entry("P1", "c-a", 1000, dimension="energi"))
led.add_realized(_entry("P1", "c-a", 1000, dimension="lisens")) # overlap: same candidate id
led.add_realized(_entry("P1", "c-b", 2500, dimension="energi"))
led.add_realized(_entry("P2", "c-c", 4000, dimension="energi"))
return led
def test_build_value_report_matches_ledger_accessors() -> None:
"""SC1: per-project + portfolio totals equal the ledger accessors; ``overlaps`` equals
``ledger.overlaps()`` (the cross-dimension overlap counted ONCE, flagged)."""
led = _mixed_ledger()
report = build_value_report(led)
for pid in {e.project_id for e in led.entries}:
assert report.per_project[pid] == led.per_project_total(pid)
assert report.portfolio_total_ore == led.portfolio_total()
assert report.overlaps == led.overlaps()
# overlap counted once: P1 = c-a (1000, once across 2 dims) + c-b (2500) = 3500
assert report.per_project["P1"] == 3500
assert report.per_project["P2"] == 4000
assert report.overlaps == [("P1", "c-a")]
def test_provenance_line_per_entry_verbatim_and_deterministic() -> None:
"""SC1 (Revision #9): ``.provenance`` carries one ``ProvenanceLine`` per ledger entry with
verbatim ``verdict_id`` + ``provenance`` string, deterministically ordered."""
led = _mixed_ledger()
report = build_value_report(led)
assert len(report.provenance) == len(led.entries)
for line in report.provenance:
assert isinstance(line, ProvenanceLine)
by_key = {(m.project_id, m.dimension, m.candidate_identity): m for m in report.provenance}
for e in led.entries:
line = by_key[(e.project_id, e.dimension, e.candidate_identity)]
assert line.verdict_id == e.verdict_id # verbatim, never re-minted
assert line.provenance == e.provenance # entry's own string, no verdict-store join
assert line.amount_ore == e.amount_ore
# deterministically ordered: same entries in a different insertion order -> identical provenance
shuffled = SavingsLedger(entries=list(reversed(led.entries)))
assert build_value_report(shuffled).provenance == report.provenance
# --- Step 2: text + JSON formatters (exact NOK bytes SC2, byte-determinism SC6) -------------------
def test_format_report_text_exact_nok_bytes() -> None:
"""SC2: a portfolio total of ``1234567`` øre renders the EXACT Norwegian NOK bytes
``"12\xa0345,67\xa0kr"`` (non-breaking-space thousands + before ``kr``, mirroring
``test_costsim.py:95``), and the empty ledger's ``0`` total renders ``"0,00\xa0kr"``."""
led = SavingsLedger(entries=[_entry("P1", "c-a", 1234567)])
text = format_report_text(build_value_report(led))
assert "12\xa0345,67\xa0kr" in text
empty = format_report_text(build_value_report(SavingsLedger()))
assert "0,00\xa0kr" in empty # valid rc-0 zero-savings output, not an error
def _sc6_entries() -> list[LedgerEntry]:
"""A determinism fixture that INCLUDES a full-``_storage_key``-duplicate pair (``c-dup`` under
``(P1, energi)`` twice, differing only in ``verdict_id``/``provenance``) — constructible ONLY by
direct ``SavingsLedger(entries=[...])`` (``add_realized`` would dedup the second). This exercises
the TOTAL provenance sort from Step 1: a triple-only sort key would TIE the pair and let
insertion order flip the bytes. Also carries a genuine cross-dimension overlap (``c-ov`` under
both ``energi`` and ``lisens``) so ``overlaps`` is non-empty."""
return [
LedgerEntry(
project_id="P1",
dimension="energi",
candidate_identity="c-dup",
amount_ore=1000,
verdict_id="v-a",
provenance="prov-a",
),
LedgerEntry(
project_id="P1",
dimension="energi",
candidate_identity="c-dup",
amount_ore=1000,
verdict_id="v-b",
provenance="prov-b", # full-key dup of the above
),
LedgerEntry(
project_id="P1",
dimension="energi",
candidate_identity="c-ov",
amount_ore=3000,
verdict_id="v-c",
provenance="prov-c",
),
LedgerEntry(
project_id="P1",
dimension="lisens",
candidate_identity="c-ov",
amount_ore=3000,
verdict_id="v-d",
provenance="prov-d", # cross-dimension overlap
),
LedgerEntry(
project_id="P2",
dimension="energi",
candidate_identity="c-c",
amount_ore=4000,
verdict_id="v-e",
provenance="prov-e",
),
]
def test_output_is_byte_deterministic_regardless_of_order() -> None:
"""SC6: the same entries inserted in a different order -> byte-identical ``format_report_text``
AND ``dump_report_json`` output. The full-key duplicate pair exercises the total provenance
sort; ``overlaps`` tuples serialize to JSON lists (asserted on the parse-back)."""
entries = _sc6_entries()
a = build_value_report(SavingsLedger(entries=list(entries)))
b = build_value_report(SavingsLedger(entries=list(reversed(entries))))
assert format_report_text(a).encode() == format_report_text(b).encode()
assert dump_report_json(a).encode() == dump_report_json(b).encode()
payload = json.loads(dump_report_json(a))
assert payload["portfolio_total_ore"] == 8000 # c-dup 1000 + c-ov 3000 (once) + P2 4000
assert payload["per_project"] == {"P1": 4000, "P2": 4000}
assert isinstance(payload["overlaps"], list) and payload["overlaps"] # non-empty
for ov in payload["overlaps"]:
assert isinstance(ov, list) # tuple serialized as a JSON array
assert ["P1", "c-ov"] in payload["overlaps"]