MAJOR-4 (misjonsreview v2 section 7), owner B = the consumer. Neither existing projection into ir.CostBaseline can serve an ingested tender corpus: cost-baseline.json is hand-written per project and baseline_from_project belongs to the road domain, so a K2-shaped bundle could be navigated and never anchored. okf.derive_cost_baseline reads the numbers already in the bundle. The premise was MEASURED before anything was built on it, and the order's two pointers named two different forms. examples/*/expected-bundle/ carry pipe tables, but every one of them is csv- or sql-sourced via render.render_table -- the xlsx path never reaches render_table at all. Measured with pandoc 3.10.2 under the producer's own writer and arguments: extract._extract_office converts, and inbox.py hands that text to render_inbox_concept untouched, so an xlsx-sourced concept file carries a pandoc SIMPLE table whose dash rule defines the column spans. A pipe-only reader would have been inert on exactly the corpus this exists for. Both forms are read, by two scanners over one role mapping and one number grammar. No judgement anywhere: the header vocabulary and the number grammar are closed, and every ambiguity refuses -- no candidate table, more than one, two columns claiming one role, two rows sharing a cost code, a row that prices nothing. A partly-priced schedule refuses in full, because a half-derived baseline anchors some codes while cost_baseline_anchored reports True. Wired behind --derive-cost-baseline and never silently: one resolution in run.py's bundle arm serves both the full run and the dry run, and the refusal propagates rather than degrading to the file loader. The two fixtures are pandoc's output verbatim, not hand-typed. The unpriced one is K2's actual pre-award shape, and the columns survive as blanks -- so the table IS a candidate and the refusal is the sharp one. Load-bearing MEASURED: 18 mutations all red against the WHOLE suite, green control 1230 passed / 5 skipped (from 1208/5, superset, 0 removed), golden demo-transcript.stdout byte-unchanged (ea8c534773acdbe41ae68f2c55724d69aaf8be4f). M17 is the one that matters for arm (b): dropping the positivity guard makes the mutant raise pydantic ValidationError, which IS a ValueError but is NOT the named class -- so pytest.raises(ValueError) would have stayed green against exactly the mutation the arm exists to catch. Verified directly, not argued. Honesty limits stated in the invariant row: NS 3451 section rows are not classified (K2 itself was not available to measure), the stamp records that a run was anchored and never which projection anchored it, the hosted surface is deliberately untouched, and no live K2 file was read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
386 lines
19 KiB
Python
386 lines
19 KiB
Python
"""``okf.derive_cost_baseline`` — the THIRD projection into ``ir.CostBaseline``, and the one that
|
||
must refuse rather than invent (MAJOR-4, misjonsreview v2 §7; owner B = the consumer, PM decision
|
||
2026-09-02).
|
||
|
||
``ir.CostBaseline``'s docstring names two projections INTO it — a bundle's hand-written
|
||
``cost-baseline.json`` (``okf.load_cost_baseline``) and the road reference domain's ``cost_items``
|
||
(``validator.baseline_from_project``). Neither can be produced from an ingested tender corpus, so
|
||
K2 could be navigated but never RUN: ``run_project``'s bundle arm resolves an OPTIONAL baseline and
|
||
a bundle carrying only ingested documents is simply un-anchored. This adds the third: derive the
|
||
baseline from a priced schedule the producer already rendered into a concept file.
|
||
|
||
**The premise was measured before anything was built on it, and the order's two pointers named two
|
||
DIFFERENT forms.** The order says "markdown-tabell rendret av produsenten fra xlsx via pandoc" and
|
||
also "les examples/ i llm-ingestion-okf for formen":
|
||
|
||
* ``examples/*/expected-bundle/`` carry **pipe** tables — but every one of them is csv- or
|
||
sql-sourced, rendered by ``render.render_table``.
|
||
* The **xlsx** path never reaches ``render_table``. ``extract._extract_office`` converts through
|
||
``pypandoc`` with ``_PANDOC_WRITER = "markdown"`` and ``_PANDOC_ARGS = ("--eol=lf",
|
||
"--wrap=none")``, and ``inbox.py`` hands that text straight to
|
||
``render_inbox_concept(decision.sanitized_text, …)``. Measured with pandoc 3.10.2 against the
|
||
producer's own ``tests/fixtures/two-line-krav.xlsx`` and against a hand-laid Prisskjema: the
|
||
result is a pandoc **simple table**, whose dash rule defines the column spans.
|
||
|
||
So a pipe-only reader would have been INERT on exactly the corpus MAJOR-4 exists for, and a
|
||
simple-only reader would be inert on the examples the order pointed at. Both forms are read, by two
|
||
scanners over ONE role mapping and ONE number grammar — the ``_split_frontmatter`` shape (one
|
||
scan, two readers), never two copies of the semantics.
|
||
|
||
**Arm (b) is the point of the whole thing.** K2 is pre-award: measured by the PM agent on
|
||
2026-09-02, ``Prisskjema.xlsx`` has 0/28 unit-price cells filled and the NS 3451 posts 01–92 carry
|
||
codes and descriptions with no quantities or prices. The columns still exist — measured here: an
|
||
empty spreadsheet cell arrives as blank space inside its span, so the header row still names
|
||
``Mengde`` and ``Enhetspris`` and the table IS a candidate. That is what makes the refusal sharp:
|
||
the deriver identifies the columns, reads empty cells, and must refuse BY NAME rather than emit a
|
||
``CostBaseline`` with an invented or zero ``unit_cost``.
|
||
|
||
``pytest.raises(ValueError)`` would NOT have gated that. ``pydantic.ValidationError`` subclasses
|
||
``ValueError`` and ``CostBaselineLine.unit_cost`` is ``Field(gt=0)``, so a deriver that fabricated
|
||
``0.0`` would raise a ``ValueError`` too and the arm would stay green against the single mutation
|
||
it exists to catch. Every refusal arm asserts the NAMED class and the token that distinguishes it.
|
||
|
||
The fixtures' numbers are chosen so a role-swap is visible: no row has ``quantity ==
|
||
unit_cost`` and the two magnitudes differ per row (kø-(y)'s ``observed != limit`` rule — a fixture
|
||
where they coincide cannot tell a correct mapping from a transposed one).
|
||
|
||
Gates ``okf.derive_cost_baseline`` + ``run.run_project(derive_cost_baseline=…)`` + the CLI door.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from portfolio_optimiser import okf, run
|
||
from portfolio_optimiser.ir import CostBaseline
|
||
|
||
_FIXTURES = Path(__file__).parent / "fixtures"
|
||
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
|
||
_UNPRICED = str(_FIXTURES / "k2-prisskjema-uprisert-SYNTETISK")
|
||
|
||
#: What the priced fixture's table says, transcribed from the fixture rather than from the
|
||
#: spreadsheet it was rendered from: arm (a) asserts the derivation equals the TABLE.
|
||
_PRICED_ROWS = {
|
||
"21.1": (1250.0, 850.0),
|
||
"24.2": (48000.0, 42.5),
|
||
"36.1": (4.0, 187500.0),
|
||
}
|
||
|
||
|
||
def _bundle(path: str) -> okf.Bundle:
|
||
return okf.navigate_bundle(path)
|
||
|
||
|
||
def _write_bundle(root: Path, body: str, *, name: str = "table.md") -> str:
|
||
root.mkdir(parents=True, exist_ok=True)
|
||
(root / "index.md").write_text(
|
||
f"---\ntype: index\ntitle: T\n---\n\n[t]({name})\n", encoding="utf-8"
|
||
)
|
||
(root / name).write_text(f"---\ntype: dataset\ntitle: T\n---\n\n{body}", encoding="utf-8")
|
||
return str(root)
|
||
|
||
|
||
# --------------------------------------------------------------------------------------------
|
||
# ARM (b) FIRST — the K2 form must refuse, typed and by name.
|
||
# --------------------------------------------------------------------------------------------
|
||
|
||
|
||
def test_b_unpriced_k2_form_refuses_by_name() -> None:
|
||
"""Codes and descriptions, EMPTY quantity and price columns: the refusal, not a baseline."""
|
||
with pytest.raises(okf.CostBaselineDerivationError) as excinfo:
|
||
okf.derive_cost_baseline(_bundle(_UNPRICED), project_id="K2")
|
||
message = str(excinfo.value)
|
||
assert "empty" in message.lower()
|
||
# Names the row AND the column: "which post" and "which cell" are two operative questions.
|
||
assert "21.1" in message
|
||
assert "Enhetspris" in message or "Mengde" in message
|
||
|
||
|
||
def test_b_the_refusal_is_the_named_class_not_a_bare_value_error() -> None:
|
||
"""``pytest.raises(ValueError)`` cannot gate this arm: ``pydantic.ValidationError`` IS a
|
||
``ValueError``, so a deriver fabricating ``unit_cost=0.0`` would satisfy it. The named class is
|
||
what separates 'we refused' from 'pydantic caught our invention on the way out'."""
|
||
assert issubclass(okf.CostBaselineDerivationError, ValueError)
|
||
with pytest.raises(okf.CostBaselineDerivationError):
|
||
okf.derive_cost_baseline(_bundle(_UNPRICED), project_id="K2")
|
||
# And the refusal is OURS, raised before any CostBaseline is constructed.
|
||
try:
|
||
okf.derive_cost_baseline(_bundle(_UNPRICED), project_id="K2")
|
||
except okf.CostBaselineDerivationError as exc:
|
||
assert type(exc).__name__ == "CostBaselineDerivationError"
|
||
|
||
|
||
def test_b_a_zero_unit_cost_is_refused_by_name_never_carried(tmp_path: Path) -> None:
|
||
"""A cell that PARSES but prices nothing. ``CostBaselineLine`` would reject it anyway — the
|
||
point is that the refusal names the column instead of surfacing as a schema error about a
|
||
number we chose to pass on."""
|
||
body = "| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | 0 |\n"
|
||
with pytest.raises(okf.CostBaselineDerivationError, match="Enhetspris"):
|
||
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "zero", body)), project_id="P")
|
||
|
||
|
||
def test_b_partial_pricing_refuses_the_WHOLE_derivation(tmp_path: Path) -> None:
|
||
"""One priced row and one unpriced row is not "a baseline with one line". A half-derived
|
||
baseline anchors some codes while ``ProvenanceStamp.cost_baseline_anchored`` reports ``True`` —
|
||
a false positive on the one bit made REQUIRED-without-default precisely because both of its
|
||
defaults would lie."""
|
||
body = (
|
||
"| Postnr | Mengde | Enhetspris |\n"
|
||
"| --- | --- | --- |\n"
|
||
"| 21.1 | 1250 | 850 |\n"
|
||
"| 24.2 | 48000 | |\n"
|
||
)
|
||
with pytest.raises(okf.CostBaselineDerivationError, match="24.2"):
|
||
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "half", body)), project_id="P")
|
||
|
||
|
||
# --------------------------------------------------------------------------------------------
|
||
# ARM (a) — a priced table derives, and derives what the table SAYS.
|
||
# --------------------------------------------------------------------------------------------
|
||
|
||
|
||
def test_a_priced_pandoc_table_derives_the_table_verbatim() -> None:
|
||
baseline = okf.derive_cost_baseline(_bundle(_PRICED), project_id="K2")
|
||
assert isinstance(baseline, CostBaseline)
|
||
assert baseline.project_id == "K2"
|
||
assert set(baseline.items) == set(_PRICED_ROWS)
|
||
for code, (quantity, unit_cost) in _PRICED_ROWS.items():
|
||
assert baseline.items[code].quantity == quantity, code
|
||
assert baseline.items[code].unit_cost == unit_cost, code
|
||
|
||
|
||
def test_a_no_row_has_quantity_equal_to_unit_cost() -> None:
|
||
"""The fixture's own discriminating property, asserted rather than assumed: if any row had
|
||
``quantity == unit_cost``, the arm above would pass against a deriver that transposed the two
|
||
columns, and the fixture would be gating nothing."""
|
||
for code, (quantity, unit_cost) in _PRICED_ROWS.items():
|
||
assert quantity != unit_cost, code
|
||
|
||
|
||
def test_a_the_pipe_table_form_derives_identically(tmp_path: Path) -> None:
|
||
"""The csv/sql-sourced form (``render.render_table``) reads through the SAME role mapping."""
|
||
body = (
|
||
"| Postnr | Beskrivelse | Mengde | Enhetspris |\n"
|
||
"| --- | --- | --- | --- |\n"
|
||
"| 21.1 | Grunnarbeider | 1250 | 850 |\n"
|
||
"| 24.2 | Staal | 48000 | 42.5 |\n"
|
||
)
|
||
baseline = okf.derive_cost_baseline(
|
||
_bundle(_write_bundle(tmp_path / "pipe", body)), project_id="P"
|
||
)
|
||
assert baseline.items["21.1"].quantity == 1250.0
|
||
assert baseline.items["21.1"].unit_cost == 850.0
|
||
assert baseline.items["24.2"].unit_cost == 42.5
|
||
|
||
|
||
# --------------------------------------------------------------------------------------------
|
||
# Determinism: no judgement. Where the columns cannot be identified UNAMBIGUOUSLY, refuse.
|
||
# --------------------------------------------------------------------------------------------
|
||
|
||
|
||
def test_no_identifiable_table_refuses(tmp_path: Path) -> None:
|
||
body = "| Fritekst | Notat |\n| --- | --- |\n| a | b |\n"
|
||
with pytest.raises(okf.CostBaselineDerivationError, match="no"):
|
||
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "none", body)), project_id="P")
|
||
|
||
|
||
def test_two_candidate_tables_refuse_rather_than_pick(tmp_path: Path) -> None:
|
||
"""Two priced schedules in one bundle is a DATA question, never one the reader answers by
|
||
order of appearance (``scripted_proposer``'s ambiguity rule)."""
|
||
root = tmp_path / "two"
|
||
body = "| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | 850 |\n"
|
||
_write_bundle(root, body, name="a.md")
|
||
(root / "b.md").write_text(
|
||
f"---\ntype: dataset\ntitle: B\n---\n\n{body}",
|
||
encoding="utf-8",
|
||
)
|
||
(root / "index.md").write_text(
|
||
"---\ntype: index\ntitle: T\n---\n\n[a](a.md)\n[b](b.md)\n", encoding="utf-8"
|
||
)
|
||
with pytest.raises(okf.CostBaselineDerivationError) as excinfo:
|
||
okf.derive_cost_baseline(_bundle(str(root)), project_id="P")
|
||
assert "a.md" in str(excinfo.value) and "b.md" in str(excinfo.value)
|
||
|
||
|
||
def test_a_duplicated_role_in_one_header_refuses(tmp_path: Path) -> None:
|
||
"""Two columns claiming the same role: a reader that took the first would be choosing."""
|
||
body = (
|
||
"| Postnr | Mengde | Enhetspris | Enhetspris |\n"
|
||
"| --- | --- | --- | --- |\n"
|
||
"| 21.1 | 1250 | 850 | 900 |\n"
|
||
)
|
||
with pytest.raises(okf.CostBaselineDerivationError, match="Enhetspris"):
|
||
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "dup", body)), project_id="P")
|
||
|
||
|
||
def test_a_duplicated_cost_code_refuses(tmp_path: Path) -> None:
|
||
"""A dict would silently last-write-win, and the baseline would quietly describe one of two
|
||
rows an operator can see in the document."""
|
||
body = (
|
||
"| Postnr | Mengde | Enhetspris |\n"
|
||
"| --- | --- | --- |\n"
|
||
"| 21.1 | 1250 | 850 |\n"
|
||
"| 21.1 | 20 | 30 |\n"
|
||
)
|
||
with pytest.raises(okf.CostBaselineDerivationError, match="21.1"):
|
||
okf.derive_cost_baseline(_bundle(_write_bundle(tmp_path / "dupcode", body)), project_id="P")
|
||
|
||
|
||
def test_a_number_outside_the_grammar_refuses(tmp_path: Path) -> None:
|
||
"""The grammar is dot-decimal, and it is closed. The measured pandoc output emits ``1250.0`` /
|
||
``42.5``, so no comma handling is needed — and admitting one would import ``1,250``'s
|
||
thousands-versus-decimal ambiguity for free, which is exactly the judgement this function is
|
||
forbidden to exercise. A hand-authored Norwegian ``1 250,50`` is a REFUSED case, stated."""
|
||
for cell in ("1 250", "850,50", "850 kr", "n/a"):
|
||
body = f"| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | {cell} |\n"
|
||
root = _write_bundle(tmp_path / f"num-{abs(hash(cell))}", body)
|
||
with pytest.raises(okf.CostBaselineDerivationError, match="Enhetspris"):
|
||
okf.derive_cost_baseline(_bundle(root), project_id="P")
|
||
|
||
|
||
def test_the_verdict_layer_is_not_scanned(tmp_path: Path) -> None:
|
||
"""Built from ``context_files``, never ``files`` (MAJOR-3's rule): a ``type: verdict`` file is
|
||
not project cost data, and a reader over ``files`` would let a prior verdict's table decide a
|
||
project's ground truth."""
|
||
root = tmp_path / "verdict"
|
||
root.mkdir()
|
||
(root / "index.md").write_text(
|
||
"---\ntype: index\ntitle: T\n---\n\n[v](v.md)\n", encoding="utf-8"
|
||
)
|
||
(root / "v.md").write_text(
|
||
"---\ntype: verdict\ntitle: V\n---\n\n"
|
||
"| Postnr | Mengde | Enhetspris |\n| --- | --- | --- |\n| 21.1 | 1250 | 850 |\n",
|
||
encoding="utf-8",
|
||
)
|
||
with pytest.raises(okf.CostBaselineDerivationError):
|
||
okf.derive_cost_baseline(_bundle(str(root)), project_id="P")
|
||
|
||
|
||
# --------------------------------------------------------------------------------------------
|
||
# CONTROL — the two existing projections are untouched, and the file remains PREFERRED.
|
||
# --------------------------------------------------------------------------------------------
|
||
|
||
|
||
def test_control_a_bundle_shipping_a_handwritten_baseline_is_untouched() -> None:
|
||
"""``load_optional_cost_baseline`` still answers, unchanged, and remains what the run path
|
||
reaches for when the flag is absent."""
|
||
shipped = okf.load_optional_cost_baseline("shared/examples/tunnel-hauglia")
|
||
assert shipped is not None
|
||
assert shipped.project_id == "TUNNEL-HAUGLIA"
|
||
assert "ENERGI-TUNNEL-EL" in shipped.items
|
||
|
||
|
||
def test_control_the_fixture_bundles_ship_no_cost_baseline_json() -> None:
|
||
"""Otherwise arm (a) could be satisfied by the file loader and prove nothing about derivation."""
|
||
for path in (_PRICED, _UNPRICED):
|
||
assert not (Path(path) / "cost-baseline.json").exists()
|
||
assert okf.load_optional_cost_baseline(path) is None
|
||
|
||
|
||
def test_control_the_fixtures_are_marked_synthetic() -> None:
|
||
readme = (_FIXTURES / "README.md").read_text(encoding="utf-8")
|
||
assert "SYNTETISK" in readme
|
||
assert "SYNTETISK" in Path(_PRICED).name
|
||
assert "SYNTETISK" in Path(_UNPRICED).name
|
||
|
||
|
||
# --------------------------------------------------------------------------------------------
|
||
# WIRING — the third projection is reachable only behind the explicit flag, never silently.
|
||
# --------------------------------------------------------------------------------------------
|
||
|
||
|
||
#: The IR projection ``_project_from_bundle`` fail-fasts on. Written HERE, in the test, and never
|
||
#: into the checked-in fixture: ``measure`` and ``claimed_saving_nok`` are the human's or the
|
||
#: mandate's to state, so a SYNTETISK K2 bundle carrying one would look like a runnable tender
|
||
#: whose candidate measure somebody authored. The derivation reads the priced TABLE; this file only
|
||
#: satisfies the pre-existing bundle contract so the wiring can be exercised at all.
|
||
_IR_PROJECTION = {
|
||
"project_id": "K2",
|
||
"measure": "PLACEHOLDER — authored by this test purely to satisfy the bundle contract",
|
||
"affected_items": [{"code": "21.1", "quantity": 1250, "unit_cost": 850.0}],
|
||
"claimed_saving_nok": 1000.0,
|
||
}
|
||
|
||
|
||
def _runnable(source: str, tmp_path: Path) -> str:
|
||
"""A copy of a fixture bundle plus the hand-authored IR projection the bundle arm requires."""
|
||
root = tmp_path / "runnable"
|
||
shutil.copytree(source, root)
|
||
(root / "validator-input.json").write_text(json.dumps(_IR_PROJECTION), encoding="utf-8")
|
||
return str(root)
|
||
|
||
|
||
async def test_run_project_uses_the_derived_baseline_when_the_flag_is_given(tmp_path: Path) -> None:
|
||
bundle_dir = _runnable(_PRICED, tmp_path)
|
||
report = await run.run_project(
|
||
"K2",
|
||
"local",
|
||
docs_dir=bundle_dir,
|
||
bundle_dir=bundle_dir,
|
||
derive_cost_baseline=True,
|
||
live_dry_run=True,
|
||
)
|
||
assert isinstance(report, run.DryRunReport)
|
||
assert report.cost_baseline_anchored is True
|
||
|
||
|
||
async def test_run_project_without_the_flag_leaves_the_fixture_unanchored(tmp_path: Path) -> None:
|
||
"""The default is the unchanged path: no ``cost-baseline.json``, so no anchoring — and the
|
||
derivation is NOT reached. Silence never derives."""
|
||
bundle_dir = _runnable(_PRICED, tmp_path)
|
||
report = await run.run_project(
|
||
"K2", "local", docs_dir=bundle_dir, bundle_dir=bundle_dir, live_dry_run=True
|
||
)
|
||
assert isinstance(report, run.DryRunReport)
|
||
assert report.cost_baseline_anchored is False
|
||
|
||
|
||
async def test_run_project_propagates_the_refusal_rather_than_falling_back(tmp_path: Path) -> None:
|
||
"""The flag is an explicit commission. A derivation that fails must not quietly degrade to the
|
||
(absent) file and report an un-anchored run: a run that goes ahead on a silently degraded
|
||
order is the defect ``load_mandate`` fail-fasts against."""
|
||
bundle_dir = _runnable(_UNPRICED, tmp_path)
|
||
with pytest.raises(okf.CostBaselineDerivationError):
|
||
await run.run_project(
|
||
"K2",
|
||
"local",
|
||
docs_dir=bundle_dir,
|
||
bundle_dir=bundle_dir,
|
||
derive_cost_baseline=True,
|
||
live_dry_run=True,
|
||
)
|
||
|
||
|
||
def test_cli_derive_requires_bundle_dir(capsys: pytest.CaptureFixture[str]) -> None:
|
||
rc = run.main(["P1", "--docs-dir", "docs", "--derive-cost-baseline"])
|
||
assert rc == 1
|
||
err = capsys.readouterr().err
|
||
# Keyed on the DISTINGUISHING token: every refusal below also contains the flag name.
|
||
assert "--bundle-dir" in err
|
||
|
||
|
||
def test_cli_derive_is_refused_in_portfolio_mode(capsys: pytest.CaptureFixture[str]) -> None:
|
||
rc = run.main(["--portfolio", "--derive-cost-baseline"])
|
||
assert rc == 1
|
||
err = capsys.readouterr().err
|
||
assert "--portfolio" in err
|
||
assert "single-project mode" in err
|
||
|
||
|
||
def test_cli_derive_is_refused_in_report_mode(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""Report mode returns BEFORE every downstream refusal, so a flag missing from
|
||
``report_forbidden`` is silently DROPPED rather than refused (F4 found exactly that gap). 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), "--derive-cost-baseline"])
|
||
assert rc == 1
|
||
assert "mode-exclusive" in capsys.readouterr().err
|