feat(prepass): --prepass-payload with six refusals by name, README and the hosted surface untouched
Steg 6 og 7 av planen. Flagget lastes fail-fast ved siden av `--mandate` (samme try/except, saa manglende/ugyldig fil lander paa `run refused:` uten traceback) og traades inn i BEGGE `run_project`-dispatchene -- dry-run og full kjoering. En egen arm SPIONERER paa argumentet, ikke paa exit-koden: et flagg som parses, valideres og droppes er F4-klassen, og de to utfallene er samme rc. SEKS NEKTER, hver ved NAVN, hver med en rc-0-kontroll paa en argv som ellers ville blitt AKSEPTERT: - `report_forbidden` -- report-modus returnerer OVER hver dispatch, saa en utelatelse er et stille DROPP. Kontrollen bruker en JSON-ARRAY-ledger; et objekt ville gjort armen roed av feil grunn (maalt i oekt 89). - `single_only` -- navngir `--portfolio`, ALDRI det delte `--prepass-payload`-tokenet: en droppet rad faller gjennom til `--bundle-dir`-kravet, hvis melding ogsaa navngir flagget, saa en arm paa det delte tokenet ville staatt groenn mot sin egen mutasjon. - krever `--bundle-dir`; nektet med `--proposals-from-mandate` (returnerer over debatten, saa flagget ville vaert stille inert), med `--dimension-config` (pre-passet kuttet uten aa kjenne dimensjoner, saa aa aere skopet ville droppe utdrag deklarasjonen teller som LEVERT -- da er nevnerne feil for kjoeringen som publiserte dem) og med `--explore` (utforskningen leser HELE basen med de fire verktoeyene payloadet trekker, saa kjoeringen som helhet ville lest langt utenfor kuttet den erklaerer). Blokka ligger paa FUNKSJONS-nivaa etter mode-dispatchen, aldri nestet under en annen grens -- under en av dem ville en bar kombinasjon falt rett gjennom. `hosting.py` er BEVISST URØRT (briefens non-goal, MAJOR-4/S7b-presedensen): feltet kommer inn i ingen av de tre settene, saa den generiske `unknown field(s)`-400-en svarer alt, og Fase 4es to halvdeler staar. Gatet av en testarm i stedet for en redigering -- inkludert den negative halvdelen (hvert videresendt felt ER en `run_project`-parameter, hvert konsumert er det ikke). README-blokka navngir alle seks partnerne, uttrykker seg i kundevendt terminologi (aldri "OKF bundle") og sier BEGGE aerlighets-grensene hoeyt: dette kjoeper et DEKLARERT kutt, ikke en billigere kjoering; og en TOM leveranse er bevis for fravaer mens en FULL ikke er bevis for tilstedevaerelse. Uttrekkeren tar BLOKKA (ikke en delstreng over hele fila -- `--portfolio` og `--report` staar overalt), med `--plan-review`-blokka som kjent-positiv kontroll. 1466 passed / 5 skipped (fra 1446/5, +20, 0 fjernet). ruff + mypy rene. Golden `shasum -a 1` av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f, BYTE-UENDRET. Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
parent
d3476d55ee
commit
ca98888358
3 changed files with 535 additions and 0 deletions
32
README.md
32
README.md
|
|
@ -481,6 +481,38 @@ when the seam is detached, so the loop cannot silently degrade into theater.
|
|||
— blocking an HTTP request on a human would also block the event loop that answers
|
||||
`/readiness`.
|
||||
|
||||
**Declaring the cut instead of navigating for it (`--prepass-payload`).** By default the debate
|
||||
is handed a pointer and reads the knowledge base itself, opening what it judges relevant. That
|
||||
works, and it leaves one thing unsaid: how much of the base was *not* read, and why. With
|
||||
`--prepass-payload` you hand the run a cut that was computed ahead of time and that states its
|
||||
own denominators — how many concepts were considered, how many delivered, how many withheld, and
|
||||
under which rule. The debate is given that cut and the navigation tools are withdrawn, because a
|
||||
debate that can still go looking is not bound by the cut it just declared.
|
||||
|
||||
The file is produced outside this framework, by any tool that emits the `okf-consumption/1`
|
||||
payload shape. Nothing is taken on trust: every delivered excerpt is checked against the document
|
||||
it names in the knowledge base — the same file, the same bytes, the same text — before a single
|
||||
model call, so a payload cannot smuggle in text the base does not contain. Expert verdicts and
|
||||
out-of-scope material are refused here exactly as the navigation tools refuse them.
|
||||
|
||||
```bash
|
||||
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD --docs-dir <docs> \
|
||||
--bundle-dir <knowledge-base> --prepass-payload cut.json --outbox-dir out --run-id r1
|
||||
```
|
||||
|
||||
The cut is printed and written to `{run_id}-prepass.json`. **This buys a declared cut, not a
|
||||
cheaper run** — the delivered text costs more than a pointer does. And note the limit it inherits:
|
||||
an *empty* delivery is evidence the base does not answer the question, but a *full* one is not
|
||||
evidence that it does. The excerpts are the closest matches, which is not the same as an answer;
|
||||
the run says so to the agents in as many words.
|
||||
|
||||
Requires `--bundle-dir`. Refused with `--portfolio` (one payload is a cut of one base at one
|
||||
point in time), with `--report` (which never runs a debate), with `--proposals-from-mandate`
|
||||
(which returns before the debate), with `--dimension-config` (the cut was computed without
|
||||
knowing about scopes, so honouring one would drop excerpts its own denominators count as
|
||||
delivered) and with `--explore` (the exploration reads the whole base with the very tools the
|
||||
payload withdraws).
|
||||
|
||||
**Answering it days later (`--checkpoint-dir` / `--resume`).** A domain expert is rarely at the
|
||||
terminal when the loop reaches the plan, so the same review can be *parked* to disk instead.
|
||||
`--checkpoint-dir` writes the suspended workflow there and the open question to
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ from portfolio_optimiser.validator import (
|
|||
validate_proposal,
|
||||
)
|
||||
from portfolio_optimiser import hitl, okf, outbox, prepass
|
||||
from portfolio_optimiser.prepass import load_prepass_payload
|
||||
from portfolio_optimiser.semretrieval import (
|
||||
SEMANTIC_WEIGHT_DEFAULT,
|
||||
Embedder,
|
||||
|
|
@ -2378,6 +2379,22 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"round trip is recorded in {run_id}-exploration.json, feedback verbatim. Input that ends "
|
||||
"without an answer is an error, NEVER a sign-off",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prepass-payload",
|
||||
default=None,
|
||||
metavar="FILE",
|
||||
help="Hand the debate a DECLARED CUT of the knowledge base instead of the pointer and the "
|
||||
"four navigator tools. FILE is one contract-conformant OKF consumption pre-pass payload "
|
||||
"(okf-consumption/1): the delivered excerpts plus the three denominators — how many "
|
||||
"concepts were considered, how many withheld, and by which rule. Every excerpt is verified "
|
||||
"against the mounted document (identity, sha256, and the delivered text re-derived from "
|
||||
"the base) before a single model call, so a payload cannot deliver bytes the base does not "
|
||||
"hold; the verdict layer and the dimension scope are refused here exactly as the navigator "
|
||||
"tools refuse them. The cut is recorded in {run_id}-prepass.json and printed. This makes "
|
||||
"the run's cut DECLARED rather than emergent; it does not make it cheaper. Requires "
|
||||
"--bundle-dir; refused with --portfolio, --report, --proposals-from-mandate, "
|
||||
"--dimension-config and --explore.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--proposal-review",
|
||||
action="store_true",
|
||||
|
|
@ -2634,6 +2651,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# MAJOR-2, and for the identical reason: report mode returns above the run dispatch, so
|
||||
# an omission here is a SILENT DROP — the door would be accepted and never asked.
|
||||
"--proposal-review": args.proposal_review,
|
||||
# The pre-pass door, listed for the same reason as its neighbours: report mode returns
|
||||
# ABOVE every dispatch, so an omission here is a silent DROP — the operator would be
|
||||
# told nothing and the declared cut would simply never happen.
|
||||
"--prepass-payload": args.prepass_payload is not None,
|
||||
# The three U12 flags, listed for exactly that reason: report mode returns before the
|
||||
# resume dispatch, so an omission here is a silent drop, not a refusal.
|
||||
"--checkpoint-dir": args.checkpoint_dir is not None,
|
||||
|
|
@ -2717,6 +2738,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# them apart. BY NAME, like its neighbours: falling through to a --bundle-dir
|
||||
# requirement would tell the operator to add a flag this mode also refuses.
|
||||
"--proposal-review": args.proposal_review,
|
||||
# ONE payload is a cut of ONE base at ONE ref, and --bundle-dir (its only source of a
|
||||
# base here) is already single-project-only. BY NAME rather than falling through to
|
||||
# the --bundle-dir requirement below, whose message ALSO names --prepass-payload: an
|
||||
# operator who wrote --portfolio --prepass-payload has to hear which of the two is
|
||||
# wrong, and an arm asserting on the shared token could not tell the two apart.
|
||||
"--prepass-payload": args.prepass_payload,
|
||||
# And the asynchronous half of the same door, on the same side of the partition and by
|
||||
# NAME for the same reason.
|
||||
"--checkpoint-dir": args.checkpoint_dir,
|
||||
|
|
@ -2767,6 +2794,54 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
return 1
|
||||
|
||||
# The pre-pass door's four remaining refusals, at FUNCTION level and never nested under
|
||||
# another flag's branch: under one, a bare combination would fall straight through to a
|
||||
# dispatch that drops the payload in silence (the F4 class).
|
||||
if not args.portfolio and args.prepass_payload is not None:
|
||||
if args.bundle_dir is None:
|
||||
print(
|
||||
"run refused: --prepass-payload requires --bundle-dir (a payload is a cut OF a "
|
||||
"knowledge base, and every excerpt in it is verified against the mounted "
|
||||
"document before the run starts; the road path has no base to verify against)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if args.proposals_from_mandate:
|
||||
# That mode settles a commission against the derived schedule and RETURNS above the
|
||||
# debate entirely, so a payload there would be accepted and silently inert.
|
||||
print(
|
||||
"run refused: --prepass-payload is not used by --proposals-from-mandate (that "
|
||||
"mode builds candidates deterministically from the commission and the derived "
|
||||
"schedule, and returns before any debate — so the cut would be declared and then "
|
||||
"never read)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if args.dimension_config is not None:
|
||||
# The pre-pass has no dimension concept, so its CUT is unscoped. Discarding delivered
|
||||
# excerpts here would make the payload's own denominators wrong for the run that
|
||||
# published them, which is the denominator failure this whole seam exists to remove.
|
||||
print(
|
||||
"run refused: --prepass-payload and --dimension-config cannot be combined (the "
|
||||
"pre-pass cut the base without knowing about dimensions, so honouring the scope "
|
||||
"would drop excerpts its declaration counts as delivered — the denominators would "
|
||||
"then be wrong for the run that published them; cut for the dimension instead, or "
|
||||
"run unscoped)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if args.explore:
|
||||
# The exploration reads the WHOLE base with all four navigator tools and then shapes
|
||||
# the mandate the debate is told it must not go outside. That is precisely the ground
|
||||
# on which the debate's own tools are withdrawn, one caller over.
|
||||
print(
|
||||
"run refused: --prepass-payload and --explore cannot be combined (the exploration "
|
||||
"navigates the whole knowledge base with the same four tools the payload "
|
||||
"withdraws, so the run as a whole would read far outside the cut it declares)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# S7b: the deterministic candidate source needs BOTH halves of its input, and each missing half
|
||||
# is refused by its own name. Neither is inferable — a commission is what a person wrote, and
|
||||
# the derived schedule is the only thing that can supply a quantity and a price — so a run that
|
||||
|
|
@ -3127,6 +3202,18 @@ def main(argv: list[str] | None = None) -> int:
|
|||
print(f"run refused: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# The declared cut, loaded fail-fast alongside the commission and for the same reason: a
|
||||
# payload that cannot be read is not a run to start with a navigating debate instead. Missing,
|
||||
# not JSON, or not the shape the models require — all three land on the refusal surface with
|
||||
# rc 1 and no traceback, which is what ``PrepassRefused`` subclasses ``ValueError`` for.
|
||||
prepass_payload: prepass.PrepassPayload | None = None
|
||||
if args.prepass_payload is not None:
|
||||
try:
|
||||
prepass_payload = load_prepass_payload(args.prepass_payload)
|
||||
except (FileNotFoundError, ValidationError, ValueError) as exc:
|
||||
print(f"run refused: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# The egress config, loaded fail-fast alongside the commission. Degrading a broken one to "no
|
||||
# external services" would make the announcement describe a run nobody configured, and a
|
||||
# partially-parsed one could contact a subset nobody chose.
|
||||
|
|
@ -3499,6 +3586,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
),
|
||||
outbox_dir=args.outbox_dir,
|
||||
run_id=args.run_id,
|
||||
prepass_payload=prepass_payload,
|
||||
verdict_input=_verdict_input_from_args(args),
|
||||
derive_cost_baseline=args.derive_cost_baseline,
|
||||
mcp_servers=mcp_servers,
|
||||
|
|
@ -3567,6 +3655,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
),
|
||||
outbox_dir=args.outbox_dir,
|
||||
run_id=args.run_id,
|
||||
prepass_payload=prepass_payload,
|
||||
verdict_input=_verdict_input_from_args(args),
|
||||
semantic_retrieval=args.semantic_retrieval,
|
||||
derive_cost_baseline=args.derive_cost_baseline,
|
||||
|
|
|
|||
414
tests/test_prepass_cli_door_loadbearing.py
Normal file
414
tests/test_prepass_cli_door_loadbearing.py
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
"""Load-bearing gate for the ``--prepass-payload`` operator door (order 20260907T080223Z).
|
||||
|
||||
Two halves, and each has its own failure mode.
|
||||
|
||||
- **The wiring.** A flag that is parsed, validated and never passed to ``run_project`` is the F4
|
||||
silent-drop class: the operator sees rc 0 and a navigating run. So one arm asserts the value
|
||||
ARRIVES, by spying on the dispatch rather than by reading an exit code.
|
||||
- **The refusals.** Six surfaces would otherwise accept the flag and do nothing with it. Each is
|
||||
refused BY NAME and each arm is paired with an **rc-0 control on an argv that would otherwise
|
||||
be ACCEPTED** — without which a red arm can come from the fixture rather than from the row.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import portfolio_optimiser.run as run_module
|
||||
from portfolio_optimiser.run import main
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "prepass" / "bygg-energi-mikro-fixture.payload.json"
|
||||
SHIPPED_BASE = Path(__file__).parent.parent / "shared" / "examples" / "bygg-energi-mikro"
|
||||
PROJECT_ID = "BYGG-KONTOR-NORD"
|
||||
|
||||
_PROPOSAL = json.dumps(
|
||||
{
|
||||
"project_id": PROJECT_ID,
|
||||
"measure": "energy_efficiency",
|
||||
"claimed_saving_nok": 30000,
|
||||
"affected_items": [{"code": "ENERGI-TOTAL-EL", "quantity": 120000.0, "unit_cost": 1.25}],
|
||||
"assumptions": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _base(tmp_path: Path) -> str:
|
||||
root = tmp_path / "mounted-under-another-name"
|
||||
shutil.copytree(SHIPPED_BASE, root)
|
||||
index = root / "index.md"
|
||||
lines = index.read_text(encoding="utf-8").split("\n")
|
||||
lines.insert(1, "bundle_id: bygg-energi-mikro-fixture")
|
||||
index.write_text("\n".join(lines), encoding="utf-8")
|
||||
return str(root)
|
||||
|
||||
|
||||
def _payload_file(tmp_path: Path) -> str:
|
||||
path = tmp_path / "payload.json"
|
||||
path.write_text(FIXTURE.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
return str(path)
|
||||
|
||||
|
||||
def _docs(tmp_path: Path) -> str:
|
||||
d = tmp_path / "docs"
|
||||
d.mkdir(exist_ok=True)
|
||||
(d / "cost.txt").write_text("Energitiltak i kontorbygg.", encoding="utf-8")
|
||||
return str(d)
|
||||
|
||||
|
||||
def _replies(tmp_path: Path) -> str:
|
||||
path = tmp_path / "replies.json"
|
||||
path.write_text(
|
||||
json.dumps({"proposer": _PROPOSAL, "checker": "VERDICT: APPROVE"}), encoding="utf-8"
|
||||
)
|
||||
return str(path)
|
||||
|
||||
|
||||
def _run_argv(tmp_path: Path, *extra: str) -> list[str]:
|
||||
"""An argv the CLI ACCEPTS — the control every refusal arm below is measured against."""
|
||||
return [
|
||||
PROJECT_ID,
|
||||
"--bundle-dir",
|
||||
_base(tmp_path),
|
||||
"--docs-dir",
|
||||
_docs(tmp_path),
|
||||
"--scripted-replies",
|
||||
_replies(tmp_path),
|
||||
*extra,
|
||||
]
|
||||
|
||||
|
||||
def _refuse_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Any model client construction becomes a failure, so a refusal that fired AFTER the spend is
|
||||
distinguishable from one that fired before it. At the exit code the two look identical."""
|
||||
|
||||
def refuse(profile: Any) -> Any:
|
||||
raise AssertionError("a model client was built despite a refusal") # pragma: no cover
|
||||
|
||||
monkeypatch.setattr(run_module, "_default_factory", refuse)
|
||||
|
||||
|
||||
# --- the wiring ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_flag_reaches_run_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Parsed-and-dropped and parsed-and-passed are the same exit code. This arm reads the
|
||||
argument, not the outcome."""
|
||||
seen: list[Any] = []
|
||||
original = run_module.run_project
|
||||
|
||||
async def spy(*args: Any, **kwargs: Any) -> Any:
|
||||
seen.append(kwargs.get("prepass_payload"))
|
||||
return await original(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(run_module, "run_project", spy)
|
||||
rc = main(_run_argv(tmp_path, "--prepass-payload", _payload_file(tmp_path)))
|
||||
assert rc == 0
|
||||
assert seen and seen[0] is not None
|
||||
assert seen[0].bundle.bundle_id == "bygg-energi-mikro-fixture"
|
||||
|
||||
|
||||
def test_the_notice_reaches_stdout(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
rc = main(_run_argv(tmp_path, "--prepass-payload", _payload_file(tmp_path)))
|
||||
assert rc == 0
|
||||
assert "DECLARED CUT" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_the_dry_run_dispatch_is_threaded_too(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""``DryRunReport.prepass`` and the dry-run notice call are dead code without this."""
|
||||
# ``--scripted-replies`` and ``--live-dry-run`` are a documented contradiction, so the dry-run
|
||||
# argv is built without it.
|
||||
rc = main(
|
||||
[
|
||||
PROJECT_ID,
|
||||
"--bundle-dir",
|
||||
_base(tmp_path),
|
||||
"--docs-dir",
|
||||
_docs(tmp_path),
|
||||
"--live-dry-run",
|
||||
"--prepass-payload",
|
||||
_payload_file(tmp_path),
|
||||
]
|
||||
)
|
||||
assert rc == 0
|
||||
assert "DECLARED CUT" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_a_missing_payload_file_refuses_without_a_traceback(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_refuse_model(monkeypatch)
|
||||
rc = main(_run_argv(tmp_path, "--prepass-payload", str(tmp_path / "nope.json")))
|
||||
assert rc == 1
|
||||
assert "run refused:" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_a_malformed_payload_file_refuses_without_a_traceback(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_refuse_model(monkeypatch)
|
||||
bad = tmp_path / "bad.json"
|
||||
bad.write_text("{not json", encoding="utf-8")
|
||||
rc = main(_run_argv(tmp_path, "--prepass-payload", str(bad)))
|
||||
assert rc == 1
|
||||
assert "run refused:" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_a_payload_for_another_base_refuses_before_any_model_call(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""``PrepassRefused`` is a ``ValueError``, so it lands on the refusal surface and not the
|
||||
crash channel — and the refusal happens before a client is built."""
|
||||
_refuse_model(monkeypatch)
|
||||
raw = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
raw["bundle"]["bundle_id"] = "a-different-corpus"
|
||||
path = tmp_path / "other.json"
|
||||
path.write_text(json.dumps(raw), encoding="utf-8")
|
||||
rc = main(_run_argv(tmp_path, "--prepass-payload", str(path)))
|
||||
assert rc == 1
|
||||
assert "run refused:" in capsys.readouterr().err
|
||||
|
||||
|
||||
# --- the six refusals, each with an rc-0 control ---------------------------------------------
|
||||
|
||||
|
||||
def test_the_flag_requires_a_bundle_dir(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_refuse_model(monkeypatch)
|
||||
rc = main(
|
||||
[
|
||||
PROJECT_ID,
|
||||
"--docs-dir",
|
||||
_docs(tmp_path),
|
||||
"--scripted-replies",
|
||||
_replies(tmp_path),
|
||||
"--prepass-payload",
|
||||
_payload_file(tmp_path),
|
||||
]
|
||||
)
|
||||
assert rc == 1
|
||||
assert "--bundle-dir" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_it_is_refused_in_portfolio_mode_by_name(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""NAMING ``--portfolio``, never the shared ``--prepass-payload`` token: a dropped row falls
|
||||
through to the ``--bundle-dir`` requirement, whose message names the flag too — so an arm
|
||||
asserting on the shared token would be green against the mutation it exists for."""
|
||||
_refuse_model(monkeypatch)
|
||||
rc = main(
|
||||
[
|
||||
"--portfolio",
|
||||
"--scripted-replies",
|
||||
_replies(tmp_path),
|
||||
"--prepass-payload",
|
||||
_payload_file(tmp_path),
|
||||
]
|
||||
)
|
||||
assert rc == 1
|
||||
assert "--portfolio" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_portfolio_mode_without_the_flag_is_accepted(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""The rc-0 control for the arm above."""
|
||||
rc = main(["--portfolio", "--scripted-replies", _replies(tmp_path)])
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def _ledger(tmp_path: Path) -> str:
|
||||
"""A JSON **ARRAY**. An object is refused by the ledger loader itself, which would make every
|
||||
report arm below red for the wrong reason (measured in økt 89)."""
|
||||
path = tmp_path / "ledger.json"
|
||||
path.write_text(json.dumps([]), encoding="utf-8")
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_it_is_refused_in_report_mode(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Report mode returns ABOVE every dispatch, so an omission here is a silent DROP."""
|
||||
_refuse_model(monkeypatch)
|
||||
rc = main(
|
||||
["--report", "--ledger", _ledger(tmp_path), "--prepass-payload", _payload_file(tmp_path)]
|
||||
)
|
||||
assert rc == 1
|
||||
assert "--report" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_report_mode_without_the_flag_is_accepted(tmp_path: Path) -> None:
|
||||
"""The rc-0 control for the arm above."""
|
||||
assert main(["--report", "--ledger", _ledger(tmp_path)]) == 0
|
||||
|
||||
|
||||
def test_it_is_refused_with_proposals_from_mandate(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""That mode returns above the debate, so the flag would be silently inert."""
|
||||
_refuse_model(monkeypatch)
|
||||
mandate = tmp_path / "m.json"
|
||||
mandate.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"objective": "x",
|
||||
"approaches": [
|
||||
{
|
||||
"id": "a1",
|
||||
"label": "energy_efficiency",
|
||||
"rationale": "r",
|
||||
"affected_codes": ["ENERGI-TOTAL-EL"],
|
||||
"claimed_saving_nok": 1000,
|
||||
}
|
||||
],
|
||||
"allow_own_proposals": False,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
rc = main(
|
||||
_run_argv(
|
||||
tmp_path,
|
||||
"--proposals-from-mandate",
|
||||
"--mandate",
|
||||
str(mandate),
|
||||
"--derive-cost-baseline",
|
||||
"--prepass-payload",
|
||||
_payload_file(tmp_path),
|
||||
)
|
||||
)
|
||||
assert rc == 1
|
||||
assert "--proposals-from-mandate" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_it_is_refused_with_a_dimension_config(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The pre-pass has no dimension concept, so its CUT is unscoped. Composing them would mean
|
||||
po discarding excerpts the declaration counted as delivered — which makes the payload's own
|
||||
denominators wrong for the run that published them."""
|
||||
_refuse_model(monkeypatch)
|
||||
dim = tmp_path / "dim.json"
|
||||
dim.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "energi",
|
||||
"label": "Energi",
|
||||
"allowed_measure_types": ["energy_efficiency"],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
rc = main(
|
||||
_run_argv(
|
||||
tmp_path, "--dimension-config", str(dim), "--prepass-payload", _payload_file(tmp_path)
|
||||
)
|
||||
)
|
||||
assert rc == 1
|
||||
assert "--dimension-config" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_a_dimension_config_without_the_flag_is_accepted(tmp_path: Path) -> None:
|
||||
"""The rc-0 control: the two flags are each fine alone."""
|
||||
dim = tmp_path / "dim.json"
|
||||
dim.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "energi",
|
||||
"label": "Energi",
|
||||
"allowed_measure_types": ["energy_efficiency"],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert main(_run_argv(tmp_path, "--dimension-config", str(dim))) == 0
|
||||
|
||||
|
||||
def test_it_is_refused_with_explore(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The exploration reads the WHOLE base with all four navigator tools and then hands its
|
||||
mandate to a debate told it is under a declared cut — this plan's own grounds for withdrawing
|
||||
the tools, one caller over."""
|
||||
_refuse_model(monkeypatch)
|
||||
rc = main(
|
||||
_run_argv(
|
||||
tmp_path, "--explore", "finn tiltak", "--prepass-payload", _payload_file(tmp_path)
|
||||
)
|
||||
)
|
||||
assert rc == 1
|
||||
assert "--explore" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_a_plain_run_without_the_flag_is_accepted(tmp_path: Path) -> None:
|
||||
"""The rc-0 control shared by the arms that add exactly one flag to this argv."""
|
||||
assert main(_run_argv(tmp_path)) == 0
|
||||
|
||||
|
||||
# --- the hosted surface, deliberately untouched ------------------------------------------------
|
||||
|
||||
|
||||
def test_the_hosted_surface_refuses_the_field_without_being_edited() -> None:
|
||||
"""The brief's Non-Goal (MAJOR-4 / S7b precedent): the field enters NONE of the three sets, so
|
||||
the generic ``unknown field(s)`` 400 already answers it and Fase 4e's two halves stand."""
|
||||
from portfolio_optimiser import hosting
|
||||
|
||||
assert "prepass_payload" not in hosting._ALLOWED_FIELDS
|
||||
with pytest.raises(ValueError, match="unknown field"):
|
||||
hosting._run_kwargs({"project_id": PROJECT_ID, "prepass_payload": "/x.json"})
|
||||
|
||||
|
||||
def test_the_phase_4e_partitions_still_hold() -> None:
|
||||
"""The negative half: every forwarded field is a real ``run_project`` parameter and every
|
||||
consumed one is not. Adding a parameter without touching hosting must not break it."""
|
||||
import inspect
|
||||
|
||||
from portfolio_optimiser import hosting
|
||||
from portfolio_optimiser.run import run_project
|
||||
|
||||
parameters = set(inspect.signature(run_project).parameters)
|
||||
assert set(hosting._REQUIRED_FIELDS) <= parameters
|
||||
assert set(hosting._OPTIONAL_FIELDS) <= parameters
|
||||
assert set(hosting._CONSUMED_FIELDS).isdisjoint(parameters)
|
||||
|
||||
|
||||
# --- the README block --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _readme_block(flag: str) -> str:
|
||||
"""The prose block for ONE flag, extracted rather than substring-matched: ``--portfolio`` and
|
||||
``--report`` occur all over the README, so a file-wide search cannot tell a documented refusal
|
||||
from an unrelated mention."""
|
||||
readme = (Path(__file__).parent.parent / "README.md").read_text(encoding="utf-8")
|
||||
start = readme.index(f"(`{flag}`)")
|
||||
end = readme.find("\n **", start)
|
||||
return readme[start : end if end != -1 else len(readme)]
|
||||
|
||||
|
||||
def test_the_readme_documents_the_flag_and_every_partner_refusal() -> None:
|
||||
block = _readme_block("--prepass-payload")
|
||||
for partner in (
|
||||
"--bundle-dir",
|
||||
"--portfolio",
|
||||
"--report",
|
||||
"--proposals-from-mandate",
|
||||
"--dimension-config",
|
||||
"--explore",
|
||||
):
|
||||
assert partner in block, partner
|
||||
# Customer-facing terminology: never "OKF bundle" on a published surface.
|
||||
assert "OKF bundle" not in block
|
||||
|
||||
|
||||
def test_the_extractor_finds_a_block_that_has_existed_since_f4() -> None:
|
||||
"""The known-positive control: an extractor that silently finds nothing would make the arm
|
||||
above green against a README with no block at all."""
|
||||
assert "--explore" in _readme_block("--plan-review")
|
||||
Loading…
Add table
Add a link
Reference in a new issue