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>
174 lines
7.1 KiB
Python
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"]
|