fix(s7b): BudgetExceeded refuses instead of tracebacking, approaches land in exploration.json
Fiks-ordre 20260904T070930Z-8335015575-from-.claude. Two scoped defects the
S7b syretest observed and left unfixed (docs/2026-09-04-syretest-s7b-k2.md
§ 3.6):
1. `BudgetExceeded` (a RuntimeError, econ 56) could leave `--explore` as a
raw Python traceback from TWO raise sites: the exploration loop's own
round/token cap (`explore()`/`resume_exploration()`, uncaught in
`main()`'s exploration `try`), and `generate_via_llm`'s retry loop when a
mandate's own-proposal evaluation hits an unparseable reply (the full-run
dispatch's `except` tuple only knew `ValueError`/`FileNotFoundError`/
`ValidationError`). Both now end as `run refused: {exc}` on stderr, rc 1,
same shape as every other loader refusal in run.py.
2. `{run_id}-exploration.json` carried rounds/tool_calls/plan_reviews/
quick_validations but not the approaches the loop actually shaped — those
stood only in the stdout mandate announcement. `explore.trace_payload`
now takes a required `mandate` keyword and renders `mandate.approaches`
under an "approaches" key, so an operator reading the artefact days later
(the whole point of the async U12 door) can recover what the run decided
to evaluate without the terminal.
Fifteen mutations across three detach points, all red against the full
suite: the exploration-loop except clause (2 red), the full-run except
tuple (1 red), and the approaches rendering (1 red) — plus a control per
fix proving the happy path is unaffected. Green control 1295 passed / 5
skipped (supersett of S7b's 1290/5, 0 removed), golden
demo-transcript.stdout byte-unchanged (shasum -a 1 = ea8c534…), ruff +
mypy clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6f8330cc61
commit
5d8844fef5
7 changed files with 479 additions and 26 deletions
|
|
@ -472,7 +472,7 @@ def test_the_exploration_artefact_carries_the_rounds_and_the_advisory_verdicts(
|
|||
|
||||
|
||||
def test_the_artefact_is_written_even_when_the_exploration_was_cut_short(
|
||||
tmp_path, monkeypatch
|
||||
tmp_path, monkeypatch, capsys
|
||||
) -> None:
|
||||
"""T13: a capped exploration is the run whose evidence matters MOST, and it is the one that
|
||||
returns nothing — so the write lives in a ``finally`` (the ``write_parse_failures`` precedent).
|
||||
|
|
@ -481,7 +481,18 @@ def test_the_artefact_is_written_even_when_the_exploration_was_cut_short(
|
|||
``stop: null`` that meant BOTH "concluded normally" and "we never found out" would be the kind
|
||||
of silence this repo writes required fields to close.
|
||||
|
||||
Detach point: move the write out of the ``finally`` → RED.
|
||||
**``main()`` no longer lets ``BudgetExceeded`` escape as a traceback** (fiks-ordre
|
||||
20260904T070930Z, observed by accident on K2's syretest): this test used to wrap the call in
|
||||
``pytest.raises(BudgetExceeded)``, which is the in-process shape of the very defect the order
|
||||
fixes — the exception reaching all the way out of ``main()`` IS the traceback a subprocess
|
||||
would print. It now asserts the typed refusal instead: rc 1, one line on stderr, same shape as
|
||||
every other loader refusal in this function. The subprocess proof that no Python traceback
|
||||
text reaches stderr lives in ``tests/test_explore_budget_refusal_loadbearing.py`` (P4's own
|
||||
"stdout/stderr is answered in a subprocess" precedent — this in-process arm cannot see printed
|
||||
text that was never printed because the exception unwound past ``capsys`` instead).
|
||||
|
||||
Detach point: move the write out of the ``finally`` → RED. Detach point for the refusal:
|
||||
remove the ``except BudgetExceeded`` clause → this arm errors instead of asserting rc 1.
|
||||
"""
|
||||
factory = _factory(
|
||||
ledgers=[_ledger_json(satisfied=False), _ledger_json(satisfied=False)],
|
||||
|
|
@ -491,29 +502,33 @@ def test_the_artefact_is_written_even_when_the_exploration_was_cut_short(
|
|||
monkeypatch.setattr("portfolio_optimiser.run._default_factory", lambda profile: factory)
|
||||
|
||||
outbox = tmp_path / "outbox"
|
||||
with pytest.raises(BudgetExceeded):
|
||||
run.main(
|
||||
[
|
||||
_PID,
|
||||
"--docs-dir",
|
||||
str(_BUNDLE_DIR),
|
||||
"--bundle-dir",
|
||||
str(_BUNDLE_DIR),
|
||||
"--explore",
|
||||
"go",
|
||||
"--explore-config",
|
||||
_config_file(tmp_path, max_rounds=2),
|
||||
"--outbox-dir",
|
||||
str(outbox),
|
||||
"--run-id",
|
||||
"r2",
|
||||
]
|
||||
)
|
||||
rc = run.main(
|
||||
[
|
||||
_PID,
|
||||
"--docs-dir",
|
||||
str(_BUNDLE_DIR),
|
||||
"--bundle-dir",
|
||||
str(_BUNDLE_DIR),
|
||||
"--explore",
|
||||
"go",
|
||||
"--explore-config",
|
||||
_config_file(tmp_path, max_rounds=2),
|
||||
"--outbox-dir",
|
||||
str(outbox),
|
||||
"--run-id",
|
||||
"r2",
|
||||
]
|
||||
)
|
||||
|
||||
assert rc == 1
|
||||
err = capsys.readouterr().err
|
||||
assert err.startswith("run refused: budget exceeded: exploration_rounds"), err
|
||||
|
||||
payload = json.loads((outbox / "r2-exploration.json").read_text(encoding="utf-8"))
|
||||
assert payload["completed"] is False
|
||||
assert payload["stop"] is None
|
||||
assert len(payload["rounds"]) == 2
|
||||
assert payload["approaches"] == [], "no mandate ever formed, so there is nothing to list"
|
||||
|
||||
|
||||
def test_the_artefact_payload_is_byte_deterministic() -> None:
|
||||
|
|
@ -537,8 +552,8 @@ def test_the_artefact_payload_is_byte_deterministic() -> None:
|
|||
bundle_id="b", proposal_json="{}", verdict={"decision": "unparseable"}
|
||||
)
|
||||
)
|
||||
first = explore.trace_payload(trace, stop=None, completed=True)
|
||||
second = explore.trace_payload(trace, stop=None, completed=True)
|
||||
first = explore.trace_payload(trace, stop=None, completed=True, mandate=None)
|
||||
second = explore.trace_payload(trace, stop=None, completed=True, mandate=None)
|
||||
assert json.dumps(first, sort_keys=True) == json.dumps(second, sort_keys=True)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue