portfolio-optimiser/tests/test_proposal_from_mandate_loadbearing.py
Kjell Tore Guttormsen 9fa1e6ced0 fix(mandate): den TREDJE doera som aapner en base faar sitt eget vitne, og ingen fabrikkert identitet
To funn fra review foer lukking av ordren.

(1) evaluate_mandate_candidates kaller assert_declared_ids_agree, men INGEN
mutasjon beviste det. S7a-3-raden enumererer doerene med vilje ("separat gir
den sin egen mutasjon per doer") fordi en uvitnet kopi kan regrere ALENE mens
de to andre staar groenne. En base hvis konsepter erklaerer to korpus ville
ellers rutet paa reconcile_bundle_id sin fallback og produsert en kandidat
tilskrevet en omstridt identitet. M16 -> 1 roed.

MAALINGEN KORRIGERTE TESTEN: foerste form erklaerte de to id-ene paa
rot-index.md og den ene konseptfila, og sto GROENN - S7a-3 holdt rot-indeksen
UTENFOR enighets-settet med vilje, fordi konsept-slaar-index er en
PRESEDENS-regel: en index i utakt med sine konsepter er fallbacken som taper,
ikke to konsepter som kolliderer. Armen bruker naa TO konseptfiler.

(2) project_id=args.project_id or "" er erstattet av en assert. Unaabar i dag
(required-args-guarden fyrer langt over), men "" ville naadd
derive_cost_baseline og myntet en CostBaseline(project_id="") - en fabrikkert
identitet, som er nettopp formen cost_baseline_anchored er
paakrevd-uten-default for aa forby. Asserten sier det i stedet for en default
som stille er uenig med den.

Load-bearing MAALT paa nytt: 16 mutasjoner, 15 ROEDE mot HELE suiten + groenn
kontroll 1275/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:39:59 +02:00

470 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""A candidate proposal that ARISES from the commission + the bundle's own priced schedule, with
no model in the loop (S7b-forberedelse, ordre ``20260903T014107Z-72282048``).
**The order's premise was felled before anything was built on it, and the measurement is what
made this two seams rather than one** (``docs/2026-09-03-forslag-fra-mandat.md``). The order says
the proposal is *read from a hand-written* ``validator-input.json``. The symptom is real — a base
without that file refuses with ``FileNotFoundError`` before the first model call — but the
diagnosis is not: ``SavingsProposal`` has ALWAYS been built by ``generate._parse_ir`` from the
MODEL's reply. The file is a fixture BESIDE the run path, required only as the project's identity
(``run._project_from_bundle``, plus two other readers). So what was missing was never a way to stop
reading a file; it was a DETERMINISTIC CANDIDATE SOURCE beside ``generate_via_llm``. This module
gates that source. Making the IR projection optional is the OTHER seam, measured in the document
and deliberately not built here.
**The fixture is used UNTOUCHED, and that is the arm's whole point.**
``test_cost_baseline_derivation_loadbearing`` has to call ``_runnable()`` — copy the fixture and
write a ``validator-input.json`` into the copy — before ``run_project`` will look at it. Here the
same fixture runs as it stands. A test that had to author that file first would have proved the
opposite of what it claims.
**Null model calls is STRUCTURAL, not merely measured.** ``run.evaluate_mandate_candidates`` is a
SYNC function: it cannot await a chat call, so no mutation of its body can quietly introduce one.
The CLI arm still asserts the property behaviourally — ``run._default_factory`` is patched to raise,
which is the seam ``test_run_cli_loadbearing`` uses, and it is the only way to prove the door
reaches the offline path rather than merely returning 0 by some other route.
**The discriminating arm is (a2), not (a).** Copying ``affected_items`` out of the derived baseline
makes stage 0 reconcile at 0 % deviation by construction, and with no assumption bands the Monte
Carlo is degenerate (P10 == P50 == P90). "The validator ran" is therefore nearly green by
construction — this repo's vacuous-gate class. The live constraint is ``MAX_SAVING_FRACTION``: a
claim above 30 % of the affected total must be REJECTED by the ordinary gate, which is what proves
the candidate goes THROUGH ``validate_proposal`` rather than around it.
Refusals assert the NAMED class and the distinguishing token, never bare ``ValueError``: pydantic's
``ValidationError`` subclasses ``ValueError`` and ``SavingsProposal`` carries two model validators,
so a source that fabricated a figure would raise ``ValueError`` too and a loose arm would stay green
against the very mutation it exists to catch (MAJOR-4's own M17 lesson).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import okf, run
from portfolio_optimiser.mandate import (
OWN_PROPOSAL_ID,
Approach,
Mandate,
MandateCandidateError,
candidate_from_approach,
)
_FIXTURES = Path(__file__).parent / "fixtures"
#: MAJOR-4's priced fixture, used EXACTLY as it sits on disk — no ``validator-input.json`` added.
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
_UNPRICED = str(_FIXTURES / "k2-prisskjema-uprisert-SYNTETISK")
_PROJECT = "K2"
#: Transcribed from the fixture's table, not from the deriver: ``21.1`` is 1250 × 850.
_LINE_211_TOTAL = 1250.0 * 850.0
#: ``validator.MAX_SAVING_FRACTION`` is 0.30, so this is the boundary the ordinary gate enforces.
_FEASIBLE_211 = _LINE_211_TOTAL * 0.30
def _baseline() -> Any:
return okf.derive_cost_baseline(okf.navigate_bundle(_PRICED), project_id=_PROJECT)
def _approach(**overrides: Any) -> Approach:
fields: dict[str, Any] = {
"id": "a1",
"label": "Redusert sprengningsvolum i sone A",
"description": "Eksperten mener massetaket kan flyttes.",
"affected_codes": ("21.1",),
"claimed_saving_nok": 200_000.0,
}
fields.update(overrides)
return Approach(**fields)
def _mandate(*approaches: Approach, allow_own: bool = False) -> Mandate:
return Mandate(
objective="Finn kostnadsbesparelser i K2",
approaches=tuple(approaches),
allow_own_proposals=allow_own,
)
# --------------------------------------------------------------------------------------------
# (a) the candidate is built from the commission + the derived baseline
# --------------------------------------------------------------------------------------------
def test_candidate_carries_the_experts_own_words_and_the_bundles_own_numbers() -> None:
"""``measure`` is the expert's label VERBATIM; the quantities are the BASELINE's, never the
approach's — the expert says WHAT and HOW MUCH, the document says at what price."""
candidate = candidate_from_approach(_approach(), baseline=_baseline(), project_id=_PROJECT)
assert candidate.project_id == _PROJECT
assert candidate.measure == "Redusert sprengningsvolum i sone A"
assert candidate.claimed_saving_nok == 200_000.0
assert [(i.code, i.quantity, i.unit_cost) for i in candidate.affected_items] == [
("21.1", 1250.0, 850.0)
]
# Stated as a measured honesty limit rather than left implicit: no band can be derived from a
# single price, so the Monte Carlo stage is degenerate on this path.
assert candidate.assumptions == {}
def test_named_codes_select_their_lines_in_the_order_the_expert_named_them() -> None:
candidate = candidate_from_approach(
_approach(affected_codes=("36.1", "21.1"), claimed_saving_nok=100_000.0),
baseline=_baseline(),
project_id=_PROJECT,
)
assert [i.code for i in candidate.affected_items] == ["36.1", "21.1"]
def test_the_ordinary_gate_validates_a_claim_inside_the_feasible_bound() -> None:
rows = run.evaluate_mandate_candidates(
_mandate(_approach()), bundle_dir=_PRICED, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [("a1", "validated")]
assert rows[0].saving_nok == 200_000.0
def test_the_ordinary_gate_rejects_a_claim_above_the_feasible_bound() -> None:
"""THE DISCRIMINATOR. Everything else about a baseline-copied candidate reconciles at 0 %
deviation by construction, so this is the one arm that can tell a candidate that goes THROUGH
``validate_proposal`` from one that goes around it. The figure is above 30 % of the affected
total and below the total itself, so pydantic's own ``claimed <= total`` validator does NOT
fire — the rejection has to come from the validator's stages."""
over = _FEASIBLE_211 + 50_000.0
assert over < _LINE_211_TOTAL # pydantic would otherwise refuse at construction
rows = run.evaluate_mandate_candidates(
_mandate(_approach(claimed_saving_nok=over)), bundle_dir=_PRICED, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [("a1", "rejected")]
assert "feasible" in rows[0].detail
# --------------------------------------------------------------------------------------------
# (b) what the expert did not say is REFUSED BY NAME, never invented
# --------------------------------------------------------------------------------------------
def test_an_approach_without_an_estimate_is_refused_by_name() -> None:
with pytest.raises(MandateCandidateError) as excinfo:
candidate_from_approach(
_approach(claimed_saving_nok=None), baseline=_baseline(), project_id=_PROJECT
)
message = str(excinfo.value)
assert "a1" in message
assert "claimed_saving_nok" in message
def test_an_approach_without_cost_codes_is_refused_by_name() -> None:
with pytest.raises(MandateCandidateError) as excinfo:
candidate_from_approach(
_approach(affected_codes=()), baseline=_baseline(), project_id=_PROJECT
)
message = str(excinfo.value)
assert "a1" in message
assert "affected_codes" in message
def test_a_code_the_baseline_does_not_carry_is_refused_by_name() -> None:
"""``route_by_bundle``'s rule, one level down: a key that names something not configured is
refused BY NAME rather than resolved by position. The known codes are listed, because the
operator's next move is to correct the mandate against the document."""
with pytest.raises(MandateCandidateError) as excinfo:
candidate_from_approach(
_approach(affected_codes=("99.9",)), baseline=_baseline(), project_id=_PROJECT
)
message = str(excinfo.value)
assert "99.9" in message
assert "21.1" in message # the known codes are named
def test_the_refusal_fires_before_any_approach_is_evaluated(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Fail-fast on a commission that cannot be executed AS WRITTEN (``load_mandate``'s rule): one
unfillable approach refuses the whole run rather than doing work for the approaches ahead of it.
The good approach is FIRST, so a source that built lazily would have solved it before raising.
**The assert is on the WORK, not on the exception**, and that is measured rather than stylistic:
an earlier version of this arm asserted only ``pytest.raises`` and stayed GREEN against the lazy
mutation, because both implementations raise and no row ever reaches the caller either way —
from the outside the two are indistinguishable. This is økt 57's rule ("a refusal after the
spend looks identical at the exit code") applied to CBC solves instead of model calls: count the
solves."""
solves = 0
real = run.validate_proposal
def _counting(*args: Any, **kwargs: Any) -> Any:
nonlocal solves
solves += 1
return real(*args, **kwargs)
monkeypatch.setattr(run, "validate_proposal", _counting)
with pytest.raises(MandateCandidateError):
run.evaluate_mandate_candidates(
_mandate(_approach(), _approach(id="a2", claimed_saving_nok=None)),
bundle_dir=_PRICED,
project_id=_PROJECT,
)
assert solves == 0
# The control: with the same wiring and a fillable commission the counter DOES move, so a
# zero above is a property of the refusal rather than of a patch that never took effect.
run.evaluate_mandate_candidates(_mandate(_approach()), bundle_dir=_PRICED, project_id=_PROJECT)
assert solves == 1
def test_an_unpriced_schedule_still_refuses_in_full() -> None:
"""MAJOR-4's refusal is not routed around: the candidate source has nothing to quantify with,
so the derivation's own named refusal propagates rather than degrading to an un-anchored run."""
with pytest.raises(okf.CostBaselineDerivationError):
run.evaluate_mandate_candidates(
_mandate(_approach()), bundle_dir=_UNPRICED, project_id=_PROJECT
)
# --------------------------------------------------------------------------------------------
# (c) the coverage report stays honest about what this path CANNOT do
# --------------------------------------------------------------------------------------------
def test_own_proposals_are_reported_as_not_evaluated_rather_than_omitted() -> None:
"""A run's OWN proposal needs a model, and this path has none. The row is REPORTED with a named
reason rather than dropped: ``ApproachOutcome``'s own rule is that an omitted row is
indistinguishable from an approach nobody commissioned. Refusing the whole run would be wrong
the other way — ``allow_own_proposals`` defaults to True, so every mandate written so far
carries it."""
rows = run.evaluate_mandate_candidates(
_mandate(_approach(), allow_own=True), bundle_dir=_PRICED, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [
("a1", "validated"),
(OWN_PROPOSAL_ID, "not_evaluated"),
]
assert "no model" in rows[1].detail
def _declaring_base(tmp_path: Path, declared: str) -> str:
"""A copy of the priced fixture that DECLARES a bundle id differing from its mount name.
Needed because the shipped fixtures declare none — S7a-3 measured zero ``^bundle_id`` matches
anywhere under ``tests/`` — so on them ``reconcile_bundle_id`` falls through to the mount's
basename and the declared id and the mount COINCIDE. A routing arm written against such a base
cannot tell the two apart, which is exactly what let the mount-name mutation stay green.
"""
import shutil
root = tmp_path / "mounted-under-another-name"
shutil.copytree(_PRICED, root)
for path in root.glob("*.md"):
text = path.read_text(encoding="utf-8")
path.write_text(
text.replace("---\ntype:", f"---\nbundle_id: {declared}\ntype:", 1), "utf-8"
)
return str(root)
def test_a_base_whose_concepts_declare_two_corpora_is_refused_at_this_door_too(
tmp_path: Path,
) -> None:
"""The THIRD door that opens a base gets its own witness (S7a-3 pkt. 1's rule, which enumerates
``run_project`` and ``read_bundle`` separately for exactly this reason: "separat gir den sin
egen mutasjon per dør").
Without the check the base would route on ``reconcile_bundle_id``'s own precedence and produce a
candidate attributed to a contested identity — the defect the agreement gate exists to stop, and
an unwitnessed call here would let this door regress alone while the other two stayed green.
TWO CONCEPT files, never a concept disagreeing with the root ``index.md``: S7a-3 kept the root
index OUT of the agreement set deliberately (concept-beats-index is a PRECEDENCE rule, so an
index out of step with its concepts is the fallback losing, not a collision). A first version of
this arm declared the two ids on the index and the one concept and stayed green for exactly that
reason — the measurement corrected the test."""
import shutil
root = tmp_path / "two-corpora"
shutil.copytree(_PRICED, root)
schedule = root / "prisskjema-SYNTETISK.md"
schedule.write_text(
schedule.read_text(encoding="utf-8").replace(
"---\ntype:", "---\nbundle_id: corpus-a\ntype:", 1
),
encoding="utf-8",
)
(root / "notat.md").write_text(
'---\nbundle_id: corpus-b\ntype: document\ntitle: "Notat"\n---\n\nEt notat.\n',
encoding="utf-8",
)
index = root / "index.md"
index.write_text(
index.read_text(encoding="utf-8") + "\nOgsaa [notat](notat.md).\n", encoding="utf-8"
)
with pytest.raises(okf.BundleIdMismatch):
run.evaluate_mandate_candidates(
_mandate(_approach()), bundle_dir=str(root), project_id=_PROJECT
)
def test_routing_follows_the_declared_id_and_refuses_the_mount_name(tmp_path: Path) -> None:
"""Routing is not re-implemented here: ``route_by_bundle`` is called with the base's DECLARED id
(S7a-3 pkt. 1), so a base delivered under a directory name of its own routes as ITSELF.
Both halves are asserted, because a mutation that routed on the mount name differs from the
shipped code in BOTH directions — and an arm that only refused an id matching neither (an
earlier version used ``f"not-{declared}"``) is green against it, since both implementations
refuse a name that is nobody's."""
declared = "k2-trinn1-20260903"
base = _declaring_base(tmp_path, declared)
assert okf.reconcile_bundle_id(base).id == declared
assert Path(base).name != declared # the two are genuinely distinct here
rows = run.evaluate_mandate_candidates(
_mandate(_approach(bundle_id=declared)), bundle_dir=base, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [("a1", "validated")]
with pytest.raises(run.MandateRoutingError):
run.evaluate_mandate_candidates(
_mandate(_approach(bundle_id=Path(base).name)),
bundle_dir=base,
project_id=_PROJECT,
)
# --------------------------------------------------------------------------------------------
# (d) the CLI door
# --------------------------------------------------------------------------------------------
def _argv(*extra: str) -> list[str]:
return [
_PROJECT,
"--docs-dir",
_PRICED,
"--bundle-dir",
_PRICED,
"--derive-cost-baseline",
"--proposals-from-mandate",
*extra,
]
def _mandate_file(tmp_path: Path, mandate: Mandate) -> str:
path = tmp_path / "mandate.json"
path.write_text(mandate.model_dump_json(), encoding="utf-8")
return str(path)
def test_cli_runs_the_whole_path_without_building_a_single_model_client(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The behavioural half of "null model calls". ``_default_factory`` is the one injection point
the CLI leaves (``test_run_cli_loadbearing``'s seam), so a door that fell through to the debate
would raise here instead of returning 0. Asserting rc 0 alone would pass against a door that
quietly did nothing."""
def _refuse(_profile: Any) -> Any:
raise AssertionError("a model client was built on a path that must make no model calls")
monkeypatch.setattr(run, "_default_factory", _refuse)
rc = run.main(_argv("--mandate", _mandate_file(tmp_path, _mandate(_approach()))))
assert rc == 0
out = capsys.readouterr().out
assert "VALIDATED" in out
assert "a1" in out
def test_cli_refuses_the_flag_without_a_mandate(capsys: pytest.CaptureFixture[str]) -> None:
rc = run.main(_argv())
assert rc == 1
assert "--mandate" in capsys.readouterr().err
def test_cli_refuses_the_flag_without_a_derived_baseline(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The derived baseline is the candidate's only source of quantities, so without it there is
nothing to build an ``affected_items`` from. Refused by NAME rather than surfacing later as a
missing-baseline failure that names neither flag."""
rc = run.main(
[
_PROJECT,
"--docs-dir",
_PRICED,
"--bundle-dir",
_PRICED,
"--proposals-from-mandate",
"--mandate",
_mandate_file(tmp_path, _mandate(_approach())),
]
)
assert rc == 1
assert "--derive-cost-baseline" in capsys.readouterr().err
def test_cli_refuses_the_flag_in_portfolio_mode(capsys: pytest.CaptureFixture[str]) -> None:
rc = run.main(["--portfolio", "--proposals-from-mandate"])
assert rc == 1
err = capsys.readouterr().err
# Named, not fallen through to "requires --mandate": an operator who wrote --portfolio has to
# hear which of the two is wrong (the --explore precedent).
assert "--portfolio" in err
assert "--proposals-from-mandate" in err
def test_cli_refuses_the_flag_in_report_mode(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Report mode returns BEFORE the run dispatch, so a flag missing from ``report_forbidden`` is
a SILENT DROP rather than a refusal (the gap F4 measured). The argv is one report mode would
otherwise ACCEPT, so rc 1 is the mutant's opposite outcome."""
ledger = tmp_path / "ledger.json"
ledger.write_text(json.dumps([]), encoding="utf-8")
assert run.main(["--report", "--ledger", str(ledger)]) == 0
capsys.readouterr()
rc = run.main(["--report", "--ledger", str(ledger), "--proposals-from-mandate"])
assert rc == 1
assert "mode-exclusive" in capsys.readouterr().err
# --------------------------------------------------------------------------------------------
# (e) the control: the hand-written projection is still preferred, and untouched
# --------------------------------------------------------------------------------------------
async def test_a_bundle_carrying_the_ir_projection_runs_exactly_as_before(tmp_path: Path) -> None:
"""The control the order asks for. This path is ADDITIVE: a base with a hand-written
``validator-input.json`` reaches the unchanged bundle arm, anchoring included. If this arm ever
goes red, the new door has started competing with the old one instead of standing beside it."""
import shutil
root = tmp_path / "runnable"
shutil.copytree(_PRICED, root)
(root / "validator-input.json").write_text(
json.dumps(
{
"project_id": _PROJECT,
"measure": "energy_efficiency",
"affected_items": [{"code": "21.1", "quantity": 1250.0, "unit_cost": 850.0}],
"claimed_saving_nok": 100_000.0,
}
),
encoding="utf-8",
)
report = await run.run_project(
_PROJECT,
"local",
docs_dir=str(root),
bundle_dir=str(root),
derive_cost_baseline=True,
live_dry_run=True,
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is True