feat(s42): --live-dry-run CLI flag + dry-run summary
This commit is contained in:
parent
0e986fe6c4
commit
d7313593bc
2 changed files with 97 additions and 2 deletions
|
|
@ -610,6 +610,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
"""Single-command console entry: run the slice for one project against a docs folder."""
|
"""Single-command console entry: run the slice for one project against a docs folder."""
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="portfolio-optimiser vertical slice")
|
parser = argparse.ArgumentParser(description="portfolio-optimiser vertical slice")
|
||||||
parser.add_argument("project_id")
|
parser.add_argument("project_id")
|
||||||
|
|
@ -626,10 +627,47 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
)
|
)
|
||||||
parser.add_argument("--decision", default="approved", choices=["approved", "rejected"])
|
parser.add_argument("--decision", default="approved", choices=["approved", "rejected"])
|
||||||
parser.add_argument("--rationale", default="reviewed by expert")
|
parser.add_argument("--rationale", default="reviewed by expert")
|
||||||
|
parser.add_argument(
|
||||||
|
"--live-dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="offline drill: build contracts/clients/budget, STOP before the first model call",
|
||||||
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
# S4.2: main() currently drives only full runs (no --live-dry-run flag yet — that is Step 3,
|
if args.live_dry_run:
|
||||||
# which replaces this cast with an isinstance(DryRunReport) branch + AZURE-refusal try/except).
|
# S4.2 drill (comparison protocol §4 pkt 2/3): walk the offline path, STOP before the first
|
||||||
|
# model call, print the run-config. A misconfigured profile (e.g. AZURE with a
|
||||||
|
# REPLACE-WITH-* placeholder) makes resolve_model raise inside the eager factory build —
|
||||||
|
# refuse cleanly (mirror preflight.main) instead of tracebacking; S4.1 is the config gate.
|
||||||
|
try:
|
||||||
|
report = asyncio.run(
|
||||||
|
run_project(
|
||||||
|
args.project_id,
|
||||||
|
args.profile,
|
||||||
|
docs_dir=args.docs_dir,
|
||||||
|
bundle_dir=args.bundle_dir,
|
||||||
|
verdict_dir=args.verdict_dir,
|
||||||
|
verdict_input={"decision": args.decision, "rationale": args.rationale},
|
||||||
|
live_dry_run=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
print(
|
||||||
|
f"live-dry-run refused: {exc}\n"
|
||||||
|
"kjør 'python -m portfolio_optimiser.preflight --profile azure' først "
|
||||||
|
"(S4.1 offline config-gate)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
assert isinstance(report, DryRunReport) # live_dry_run=True always returns a DryRunReport
|
||||||
|
print(
|
||||||
|
f"{args.project_id}: LIVE-DRY-RUN OK (profile={report.profile}, "
|
||||||
|
f"models={report.resolved_models}, max_rounds={report.max_rounds}, "
|
||||||
|
f"max_tokens={report.max_tokens}, top_k={report.top_k}) — "
|
||||||
|
"ingen modellkall gjort (stoppet før første debate.run)"
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
result = cast(
|
result = cast(
|
||||||
RunResult,
|
RunResult,
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
|
|
|
||||||
57
tests/test_live_dry_run.py
Normal file
57
tests/test_live_dry_run.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""S4.2 CLI surface for ``python -m portfolio_optimiser.run <proj> --live-dry-run``.
|
||||||
|
|
||||||
|
The success arm proves the CLI wiring + rc + summary (the zero-chat-call guarantee itself is proven
|
||||||
|
at ``run_project`` level in ``test_live_dry_run_loadbearing.py`` — ``main()`` exposes no
|
||||||
|
``client_factory`` seam). The refusal arm proves a misconfigured profile (the AZURE placeholder map)
|
||||||
|
yields a STRUCTURED refusal + ``rc 1`` pointing at preflight, never a raw traceback.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from portfolio_optimiser import run
|
||||||
|
|
||||||
|
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_live_dry_run_ok(capsys) -> None:
|
||||||
|
"""Success arm: LOCAL profile (offline client construction) + ``--live-dry-run`` → rc 0 and a
|
||||||
|
summary naming the profile + a no-model-call note. No socket (stops before ``debate.run``)."""
|
||||||
|
rc = run.main(
|
||||||
|
[
|
||||||
|
"BYGG-KONTOR-NORD",
|
||||||
|
"--docs-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--bundle-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--live-dry-run",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "LIVE-DRY-RUN OK" in out
|
||||||
|
assert "local" in out
|
||||||
|
assert "ingen modellkall" in out.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_live_dry_run_azure_placeholder_refuses(capsys) -> None:
|
||||||
|
"""Refusal arm: AZURE profile with the default bundled placeholder map → ``resolve_model`` raises
|
||||||
|
inside the eager factory build; ``main()`` catches it and prints a structured refusal to stderr
|
||||||
|
+ returns rc 1 (pointing at preflight), never a traceback. Offline (pure map read)."""
|
||||||
|
rc = run.main(
|
||||||
|
[
|
||||||
|
"BYGG-KONTOR-NORD",
|
||||||
|
"--docs-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--bundle-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--profile",
|
||||||
|
"azure",
|
||||||
|
"--live-dry-run",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert rc == 1
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert "refused" in err.lower()
|
||||||
|
assert "preflight" in err.lower()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue