feat(s7b): IR-projeksjonen er valgfri paa alle tre kallsteder, og fraveret sies
DEL A + DEL C av ordre 20260903T204605Z-215167684-from-.claude.
Soem 1: okf.load_optional_ir_projection ved siden av den fail-faste, moensteret
fra load_cost_baseline/load_optional_cost_baseline. Et PAR, ikke et required=-flagg:
PM-tillegg 5 maalte hva en uoevet parameter koster (elleve evidence_for-kallsteder
brukte defaulten til den andre grenen raatnet), og et flagg ville dessuten gjort
usanne de fem docstringene som siterer load_ir_projection som DEN fail-faste
presedensen. Toleransen stopper ved fravaer: en malformed projeksjon reiser fortsatt.
De tre kallstedene fikk HVER SIN stilling:
* _project_from_bundle - fravaer hopper over en fail-fast som ikke har noe aa
sjekke mot; en projeksjon som FINNES og navngir et annet prosjekt nekter
fortsatt. Divergens-vakten er kontrakten multi-base-dispatchen hviler paa.
* run_mandate_across_bundles - FILA FOERST, basens ERKLAERTE bundle_id som
fallback (S7a-3). Presedensen baerer i begge retninger: erklaering-foerst ville
re-adressert hver eksisterende base der project_id != bundle_id.
* bundle_candidate_features - optional_bundle_candidate_features, og de TO
konsumentene svarer ULIKT paa fravaeret. Steg-1-folden HOPPER OVER og sier
hvor mange tidligere dommer som dermed aldri naadde hypotese-prompten;
seed_store_from_bundle NEKTER ved navn (VerdictKeyUnavailable), fordi aa mynte
en noekkel for en dom som erklaerer ingen er nettopp defekten S3.2 lukker.
Synligheten: RunResult.unkeyed_verdicts (ANTALL, ikke flagg - koe-(y)-regelen) +
run.unkeyed_verdicts_notice som ENESTE renderer, None ved null (omisjon, aldri tom
rad). Baereren er MAALT: dry-run-kuttet returnerer OVER folden, saa et felt paa
DryRunReport kunne bare rapportert null - ulikt cost_baseline_anchored og
skipped_links, begge opploest over kuttet. Ikke paa ProvenanceStamp: stempelet
beskriver gaten som doemte EN kandidat.
Prosjektnavnet var et ikke-spoersmaal, og det er maalt: SavingsProposal har intet
navnefelt, saa projeksjonen har aldri vaert en navnekilde. Project.name kommer
fortsatt fra type: project-konseptets title med id-en som siste utvei.
DEL C: --mandate lagt i report_forbidden. Den var ELDRE enn partisjonen og hadde
aldri faatt en rad, saa --report --ledger X --mandate Y droppet kommisjonen i
STILLHET - F4-klassen. Testarmen kjoerer mot en argv report-modus ellers ville
AKSEPTERT, med en kontroll som beviser rc 0 uten flagget.
Kontroll: 1290 passed / 5 skipped (fra 1275/5 - supersett, 0 fjernet).
Golden demo-transcript.stdout BYTE-UENDRET, shasum -a 1 (INNHOLD, ikke git-blob)
= ea8c534773acdbe41ae68f2c55724d69aaf8be4f. ruff + mypy rene.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
26d08e7a96
commit
b75387ca25
4 changed files with 593 additions and 14 deletions
439
tests/test_optional_ir_projection_loadbearing.py
Normal file
439
tests/test_optional_ir_projection_loadbearing.py
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
"""The hand-written IR projection is OPTIONAL, and its absence is said rather than raised
|
||||
(S7b søm 1, ordre ``20260903T204605Z-215167684-from-.claude`` DEL A).
|
||||
|
||||
**The seam this closes was measured before it was built** (``docs/2026-09-03-forslag-fra-mandat.md``
|
||||
§ 1.1): ``okf.load_ir_projection`` is fail-fast required at THREE call sites, and an ingested tender
|
||||
corpus carries no hand-written ``validator-input.json``, so such a base could be navigated
|
||||
(``read_bundle``), catalogued (``list_bundles``) and judged through the deterministic mandate door
|
||||
(``evaluate_mandate_candidates``, which never reads the file) — and still could not run the
|
||||
eight-step loop at all. The refusal fired before the first model call, which is the one mercy it
|
||||
had.
|
||||
|
||||
**The pattern is the repo's own, not a new one.** ``load_cost_baseline`` /
|
||||
``load_optional_cost_baseline`` already say exactly this: ABSENCE is tolerated and the caller takes
|
||||
a stance; a file that EXISTS but is malformed, or that disagrees with the run, still refuses. The
|
||||
S4.0 row states the rule in one line — *"toleransen stopper ved fravær"* — and reading a corrupt
|
||||
projection as "no projection" would hand back an unkeyed run wearing a keyed one's clothes.
|
||||
|
||||
**A pair, never a ``required=`` flag.** PM-tillegg 5 measured what an unexercised parameter costs:
|
||||
all eleven ``evidence_for`` call sites used the default, so the other branch rotted until it raised.
|
||||
A pair also keeps true the five docstrings across ``hitl``/``ledger``/``verdicts``/``contracts`` that
|
||||
cite ``load_ir_projection`` as *the* fail-fast precedent.
|
||||
|
||||
**The project NAME was a non-question, and that is measured rather than assumed.** The archived
|
||||
self-order flagged the fallback as a real decision. Measured: ``SavingsProposal`` has five fields
|
||||
(``project_id``, ``measure``, ``affected_items``, ``claimed_saving_nok``, ``assumptions``) and none
|
||||
of them is a name — the projection has never been a name source. ``Project.name`` comes from the
|
||||
``type: project`` concept's ``title`` with the id as last resort, before and after, so there is no
|
||||
new implicit ``project_id`` here; there is an old one, unchanged.
|
||||
|
||||
**Each of the three sites needed its OWN stance, which is why this is not one loader swap.**
|
||||
|
||||
* ``run._project_from_bundle`` — absence skips a fail-fast that has nothing to check against;
|
||||
a projection that is PRESENT and names another project still refuses. That divergence guard is
|
||||
what multi-base dispatch rests on ("den eksisterende fail-fasten blir rutingsnøkkelen"), so
|
||||
loosening absence must not loosen disagreement.
|
||||
* ``run.run_mandate_across_bundles`` — the routed run's ``project_id``. FILE FIRST, the base's
|
||||
DECLARED ``bundle_id`` as the fallback (S7a-3). The precedence is load-bearing in the other
|
||||
direction too: every existing base whose ``project_id`` differs from its ``bundle_id`` must keep
|
||||
routing on the file.
|
||||
* ``verdicts.bundle_candidate_features`` — the ExpeL query key, and the ONE place where absence is
|
||||
not merely tolerable. Two consumers, two different answers: the run's Step-1 fold SKIPS and SAYS
|
||||
how many prior verdicts did not reach the prompt, while ``seed_store_from_bundle``'s S3.2 fallback
|
||||
REFUSES BY NAME, because minting a key for a verdict that declares none is precisely the defect
|
||||
S3.2 closes.
|
||||
|
||||
**The visibility carrier is ``RunResult`` alone, and that is a measurement.**
|
||||
``cost_baseline_anchored`` and ``skipped_links`` are both resolved above the ``live_dry_run`` cut
|
||||
(``run.py``: the cut returns before generation), so both can be carried on ``DryRunReport``. The
|
||||
ExpeL fold happens BELOW that cut — a dry run stops before it — so a ``DryRunReport`` field could
|
||||
only ever report zero. It is not on ``ProvenanceStamp`` either, for ``skipped_links``' own reason:
|
||||
this is a RUN-level fact settled once, before any candidate exists, whereas the stamp describes the
|
||||
gate that judged ONE candidate.
|
||||
|
||||
**Predictable vacuity, hunted for deliberately.** The shipped fixtures declare no ``bundle_id``
|
||||
(S7a-3 measured zero ``^bundle_id`` matches under ``tests/``), so on them the declared id and the
|
||||
mount basename COINCIDE and a routing arm cannot tell the two apart — every routing arm here
|
||||
therefore runs against a base crafted to declare an id differing from its directory name (M14's
|
||||
lesson). And an arm that asserts only ``pytest.raises`` cannot discriminate when both
|
||||
implementations raise (M5's lesson), so the refusal arms assert the NAMED class plus a
|
||||
distinguishing token.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from portfolio_optimiser import okf, run, verdicts
|
||||
from portfolio_optimiser.mandate import Approach, Mandate
|
||||
from portfolio_optimiser.run import RunResult
|
||||
from portfolio_optimiser.simulation import scripted_factory
|
||||
from portfolio_optimiser.verdicts import ProposalFeatures, VerdictStore, capture_verdict
|
||||
|
||||
_FIXTURES = Path(__file__).parent / "fixtures"
|
||||
#: MAJOR-4's priced fixture. It ships WITHOUT a ``validator-input.json`` — that absence is the whole
|
||||
#: subject here, so it is used exactly as it sits on disk unless an arm says otherwise.
|
||||
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
|
||||
_PROJECT = "K2"
|
||||
|
||||
#: A proposal the deterministic gate validates against the fixture's derived schedule: 21.1 is
|
||||
#: 1250 x 850 = 1 062 500, and ``energy_efficiency`` caps the claim at 15 % of that (F8's registry).
|
||||
_PROPOSAL_JSON = 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,
|
||||
"assumptions": {"21.1": [800.0, 900.0]},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _copy(tmp_path: Path, name: str, *, declared: str | None = None) -> Path:
|
||||
"""A working copy of the priced fixture, optionally DECLARING a bundle id that differs from its
|
||||
mount name — the only shape under which a routing arm can distinguish declaration from mount."""
|
||||
root = tmp_path / name
|
||||
shutil.copytree(_PRICED, root)
|
||||
if declared is not None:
|
||||
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 root
|
||||
|
||||
|
||||
def _write_projection(root: Path, project_id: str) -> None:
|
||||
(root / "validator-input.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": project_id,
|
||||
"measure": "energy_efficiency",
|
||||
"affected_items": [{"code": "21.1", "quantity": 1250.0, "unit_cost": 850.0}],
|
||||
"claimed_saving_nok": 100_000.0,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _factory(sink: list[str] | None = None) -> Any:
|
||||
return scripted_factory(
|
||||
{"proposer": _PROPOSAL_JSON, "checker": "VERDICT: APPROVE"},
|
||||
[] if sink is None else sink,
|
||||
)
|
||||
|
||||
|
||||
async def _run(bundle_dir: str, *, sink: list[str] | None = None, **kwargs: Any) -> Any:
|
||||
return await run.run_project(
|
||||
_PROJECT,
|
||||
"local",
|
||||
docs_dir=bundle_dir,
|
||||
bundle_dir=bundle_dir,
|
||||
derive_cost_baseline=True,
|
||||
client_factory=_factory(sink),
|
||||
max_rounds=2,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# (a) the loader pair itself
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_optional_loader_returns_none_for_a_base_that_has_no_projection() -> None:
|
||||
assert okf.load_optional_ir_projection(_PRICED) is None
|
||||
with pytest.raises(FileNotFoundError):
|
||||
okf.load_ir_projection(_PRICED)
|
||||
|
||||
|
||||
def test_the_optional_loader_returns_the_projection_when_the_base_has_one(tmp_path: Path) -> None:
|
||||
root = _copy(tmp_path, "with-projection")
|
||||
_write_projection(root, _PROJECT)
|
||||
loaded = okf.load_optional_ir_projection(str(root))
|
||||
assert loaded is not None
|
||||
assert loaded["project_id"] == _PROJECT
|
||||
|
||||
|
||||
def test_the_tolerance_stops_at_absence_a_malformed_projection_still_raises(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""S4.0's rule, transplanted verbatim: reading a corrupt projection as "no projection" would
|
||||
hand back an unkeyed run under the appearance of a keyed one."""
|
||||
root = _copy(tmp_path, "malformed")
|
||||
(root / "validator-input.json").write_text("{not json", encoding="utf-8")
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
okf.load_optional_ir_projection(str(root))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# (b) the run path: a base with no projection runs the WHOLE loop
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_a_base_without_a_projection_runs_the_whole_pipeline() -> None:
|
||||
"""THE HEADLINE. Before this seam the same call raised ``FileNotFoundError`` from
|
||||
``_project_from_bundle`` — before the first model call, which is why an ingested corpus could be
|
||||
navigated and never run."""
|
||||
result = await _run(_PRICED)
|
||||
assert isinstance(result, RunResult)
|
||||
assert result.provenance.validator_decision in {"validated", "rejected"}
|
||||
assert result.provenance.cost_baseline_anchored is True
|
||||
|
||||
|
||||
async def test_a_projection_that_names_another_project_still_refuses(tmp_path: Path) -> None:
|
||||
"""The divergence guard, which multi-base dispatch rests on. Absence became tolerable;
|
||||
DISAGREEMENT did not, and a loader swap that loosened both would be green on every other arm
|
||||
here."""
|
||||
root = _copy(tmp_path, "wrong-project")
|
||||
_write_projection(root, "SOMEONE-ELSE")
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
await _run(str(root))
|
||||
message = str(excinfo.value)
|
||||
assert "SOMEONE-ELSE" in message
|
||||
assert _PROJECT in message
|
||||
|
||||
|
||||
async def test_a_base_that_carries_the_projection_runs_exactly_as_before(tmp_path: Path) -> None:
|
||||
"""The control the order asks for: with the file present nothing about the run changes."""
|
||||
root = _copy(tmp_path, "runnable")
|
||||
_write_projection(root, _PROJECT)
|
||||
result = await _run(str(root))
|
||||
assert isinstance(result, RunResult)
|
||||
assert result.provenance.validator_decision in {"validated", "rejected"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# (c) the dispatcher's routing key: file first, declared bundle id as the fallback
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_the_dispatcher_routes_a_projectionless_base_on_its_declared_id(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""``run_mandate_across_bundles`` takes no ``project_id`` parameter BY DESIGN — it reads each
|
||||
base's own. Without a projection the base's DECLARED id is what remains, and S7a-3 already made
|
||||
that the identity every other door uses."""
|
||||
declared = "k2-trinn1-20260903"
|
||||
root = _copy(tmp_path, "mounted-elsewhere", declared=declared)
|
||||
assert Path(root).name != declared # the two are genuinely distinct here
|
||||
|
||||
result = await run.run_mandate_across_bundles(
|
||||
Mandate(
|
||||
objective="Finn besparelser",
|
||||
approaches=(Approach(id="a1", label="Tiltak", bundle_id=declared),),
|
||||
allow_own_proposals=True,
|
||||
),
|
||||
(str(root),),
|
||||
"local",
|
||||
client_factory=_factory(),
|
||||
max_rounds=2,
|
||||
)
|
||||
assert [r.bundle_id for r in result.runs] == [declared]
|
||||
assert [r.project_id for r in result.runs] == [declared]
|
||||
|
||||
|
||||
async def test_the_dispatcher_still_prefers_the_projection_when_the_base_has_one(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""PRECEDENCE, and it is load-bearing in the direction the headline arm cannot see: every base
|
||||
whose ``project_id`` differs from its ``bundle_id`` must keep routing on the FILE. The base here
|
||||
declares both, and they disagree — an implementation that read the declared id first would send
|
||||
the run under the wrong project name while every other arm stayed green."""
|
||||
declared = "k2-trinn1-20260903"
|
||||
root = _copy(tmp_path, "both-known", declared=declared)
|
||||
_write_projection(root, _PROJECT)
|
||||
assert _PROJECT != declared
|
||||
|
||||
result = await run.run_mandate_across_bundles(
|
||||
Mandate(
|
||||
objective="Finn besparelser",
|
||||
approaches=(Approach(id="a1", label="Tiltak", bundle_id=declared),),
|
||||
allow_own_proposals=True,
|
||||
),
|
||||
(str(root),),
|
||||
"local",
|
||||
client_factory=_factory(),
|
||||
max_rounds=2,
|
||||
)
|
||||
assert [r.project_id for r in result.runs] == [_PROJECT]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# (d) the ExpeL fold: skipped for want of a key, and SAID
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
#: A rationale that appears nowhere in the fixture, so its presence in a prompt can only come from
|
||||
#: the ExpeL fold (``simulate_learning_loop``'s marker rule).
|
||||
_MARKER = "TIDLIGERE-DOM-MARKOER"
|
||||
|
||||
|
||||
def _store() -> VerdictStore:
|
||||
return VerdictStore(
|
||||
verdicts=[
|
||||
capture_verdict(
|
||||
ProposalFeatures(
|
||||
affected_codes=frozenset({"21.1"}),
|
||||
measure_type="energy_efficiency",
|
||||
claimed_saving_nok=90_000.0,
|
||||
),
|
||||
"approved",
|
||||
_MARKER,
|
||||
),
|
||||
capture_verdict(
|
||||
ProposalFeatures(
|
||||
affected_codes=frozenset({"36.1"}),
|
||||
measure_type="ventilation",
|
||||
claimed_saving_nok=50_000.0,
|
||||
),
|
||||
"rejected",
|
||||
"tidligere dom nummer to",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def test_prior_verdicts_that_could_not_be_keyed_are_counted_not_silently_dropped() -> None:
|
||||
"""Without a projection there is no pre-hypothesis candidate to key the fold against, so the
|
||||
fold cannot run. The run says how MANY prior verdicts therefore never reached the hypothesis
|
||||
prompt — the ``BudgetExceeded`` kø-(y) rule applied to a fold: "it did not happen" and "you lost
|
||||
two judgements" are different operative facts."""
|
||||
store = _store()
|
||||
sink: list[str] = []
|
||||
result = await _run(_PRICED, store=store, sink=sink)
|
||||
assert isinstance(result, RunResult)
|
||||
assert result.unkeyed_verdicts == 2
|
||||
# THE DISCRIMINATOR: a counter can be wired to anything, so the arm reads the PROMPT the fold
|
||||
# would have written into. The marker is a rationale that exists only in this store.
|
||||
assert not any(_MARKER in prompt for prompt in sink)
|
||||
|
||||
|
||||
async def test_a_run_whose_fold_was_keyed_counts_none(tmp_path: Path) -> None:
|
||||
"""The control. A constant-count implementation and a constant-zero one are both green without
|
||||
this pair — and it is what makes the renderer's OMISSION itself gated."""
|
||||
root = _copy(tmp_path, "keyed")
|
||||
_write_projection(root, _PROJECT)
|
||||
sink: list[str] = []
|
||||
result = await _run(str(root), store=_store(), sink=sink)
|
||||
assert isinstance(result, RunResult)
|
||||
assert result.unkeyed_verdicts == 0
|
||||
# And the fold genuinely RAN — without this the arm above would be green against an
|
||||
# implementation that never folds at all and merely counts zero here.
|
||||
assert any(_MARKER in prompt for prompt in sink)
|
||||
|
||||
|
||||
def test_the_renderer_is_silent_when_every_prior_verdict_was_keyed() -> None:
|
||||
"""Omission, never an empty row (``mandate.announce``'s rule, the one every ``*_notice``
|
||||
renderer here follows)."""
|
||||
assert run.unkeyed_verdicts_notice(0) is None
|
||||
line = run.unkeyed_verdicts_notice(2)
|
||||
assert line is not None
|
||||
assert "2" in line
|
||||
|
||||
|
||||
def test_the_cli_prints_the_notice_for_a_projectionless_base_with_prior_verdicts(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""The renderer reaching stdout is its own seam (the ``cost_baseline_notice`` precedent: a
|
||||
renderer nothing calls is a line nobody sees). Driven through ``main()`` with the verdict inbox,
|
||||
which is the only route by which the CLI can hand ``run_project`` a non-empty store."""
|
||||
inbox = tmp_path / "inbox"
|
||||
inbox.mkdir()
|
||||
verdicts.write_verdict(str(inbox), _store().verdicts[0])
|
||||
|
||||
monkeypatch.setattr(run, "_default_factory", lambda _profile: _factory())
|
||||
rc = run.main(
|
||||
[
|
||||
_PROJECT,
|
||||
"--docs-dir",
|
||||
_PRICED,
|
||||
"--bundle-dir",
|
||||
_PRICED,
|
||||
"--derive-cost-baseline",
|
||||
"--verdict-dir",
|
||||
str(inbox),
|
||||
]
|
||||
)
|
||||
assert rc == 0
|
||||
assert "1 prior expert verdict" in capsys.readouterr().out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# (e) the seed fallback: REFUSED by name, never keyed on a guess
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _verdict_concept(root: Path, name: str, *, structural: bool) -> None:
|
||||
key = (
|
||||
'affected_codes: "21.1"\nmeasure_type: "energy_efficiency"\nclaimed_saving_nok: "90000"\n'
|
||||
if structural
|
||||
else ""
|
||||
)
|
||||
(root / name).write_text(
|
||||
f'---\ntype: verdict\ntitle: "Dom"\n{key}decision: approved\n---\n\nEn tidligere dom.\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
index = root / "index.md"
|
||||
index.write_text(
|
||||
index.read_text(encoding="utf-8") + f"\nOgsaa [dom]({name}).\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def test_a_verdict_without_its_own_key_in_a_projectionless_base_is_refused_by_name(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""S3.2's rule at the one point where absence is NOT tolerable. The pre-S3.2 fallback keys such
|
||||
a verdict on the bundle's projection candidate; with no projection there is no candidate, and
|
||||
minting one would attach the verdict to a candidate it is not about — the exact defect S3.2
|
||||
closes. Validation, never repair (``write_concept_file``'s rule)."""
|
||||
root = _copy(tmp_path, "unkeyable")
|
||||
_verdict_concept(root, "dom.md", structural=False)
|
||||
with pytest.raises(verdicts.VerdictKeyUnavailable) as excinfo:
|
||||
verdicts.seed_store_from_bundle(str(root))
|
||||
message = str(excinfo.value)
|
||||
assert "dom.md" in message
|
||||
assert "affected_codes" in message
|
||||
|
||||
|
||||
def test_a_verdict_that_declares_its_own_key_seeds_without_any_projection(tmp_path: Path) -> None:
|
||||
"""The control, and the reason the refusal above is NARROW: S3.2's fields are what
|
||||
``promote_verdict`` writes, so a base grown by the loop itself seeds unchanged."""
|
||||
root = _copy(tmp_path, "self-keyed")
|
||||
_verdict_concept(root, "dom.md", structural=True)
|
||||
store = verdicts.seed_store_from_bundle(str(root))
|
||||
assert len(store.verdicts) == 1
|
||||
assert store.verdicts[0].proposal_features.affected_codes == frozenset({"21.1"})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# (f) DEL C — the report-mode partition (the gap økt 82 measured and reported)
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_report_mode_refuses_a_mandate_instead_of_dropping_it(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""``--mandate`` was the one flag older than ``report_forbidden`` that never got a row, so
|
||||
``--report --ledger X --mandate Y`` announced nothing and settled nothing — a SILENT DROP, the
|
||||
F4 class. The argv is one report mode would otherwise ACCEPT (a valid ``--ledger``, and the
|
||||
control proves rc 0 without the flag), so rc 1 is the mutant's opposite outcome rather than the
|
||||
same refusal arriving by another route."""
|
||||
ledger = tmp_path / "ledger.json"
|
||||
ledger.write_text(json.dumps([]), encoding="utf-8")
|
||||
mandate = tmp_path / "mandate.json"
|
||||
mandate.write_text(
|
||||
Mandate(objective="x", approaches=(Approach(id="a1", label="t"),)).model_dump_json(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert run.main(["--report", "--ledger", str(ledger)]) == 0
|
||||
capsys.readouterr()
|
||||
rc = run.main(["--report", "--ledger", str(ledger), "--mandate", str(mandate)])
|
||||
assert rc == 1
|
||||
assert "mode-exclusive" in capsys.readouterr().err
|
||||
Loading…
Add table
Add a link
Reference in a new issue