fix(major2): en skrivefeil i finally fortrenger ikke lenger stoppet i flukt
Funn d71a5d72 (MINOR, MISSING_ERROR_HANDLING, run.py:1202). Begge finally-skriverne ligger i propageringsstien til nettopp det unntaket de er bevis for: en OSError fra mkdir/write_text mens BudgetExceeded eller ProposalReviewInputError er i flukt ERSTATTER den - og hverken `except ProposalReviewInputError` (:3404) eller nekt-tuppelen (:3412) fanger OSError, saa operatoeren fikk traceback og grunnen til at kjoeringen stoppet var borte. Review-skriveren gaar paa HVER kjoering med reviewer; parse-skriveren har samme form. `_write_or_report` er EN kopi for begge kallstedene (koe-(p)): en regel om hva en skriver faar gjoere med et unntak i flukt, kopiert, blir en regel anvendt paa bare det ene. Vakten er BETINGET, aldri en blanket except - uten noe i flukt finnes ingen stoppgrunn aa beskytte, og en kjoering som ikke fikk skrevet utboksen maa si fra ved aa feile. Feilen SIES uansett, fordi et fravaerende artefakt ellers leses som en kjoering uten noe aa registrere (T10/T11). `in_flight` fanges eksplisitt (`except BaseException as stop: ... raise`), ikke via `sys.exc_info()`, som ville lest et ytre except-lag hos en bibliotekkaller som en flukt her. MAALT mot HELE suiten, to mutasjoner, hver med sin egen signatur, kontroll 1367 passed / 5 skipped og golden `demo-transcript.stdout` BYTE-UENDRET (`shasum -a 1` av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f): MC vakten detached (2 roede - de to in-flight-armene) - MD svelg ubetinget (1 roed - KONTROLL-armen alene, altsaa er betingelsen selv gatet). Iron Law: begge in-flight-armene skrevet FOERST og maalt roede mot uendret run.py; kontroll-armen var groenn foer fiksen, som er nettopp diskrimineringen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d4a0220b68
commit
257f44f0cb
2 changed files with 136 additions and 9 deletions
|
|
@ -884,6 +884,89 @@ async def test_t9_a_budget_stop_inside_generation_still_leaves_the_record(tmp_pa
|
|||
]
|
||||
|
||||
|
||||
class _WriteRefused(OSError):
|
||||
"""A disk that will not take the artefact. Its own subclass so the control arm can assert that
|
||||
THIS error escaped, rather than that some ``OSError`` did."""
|
||||
|
||||
|
||||
def _refuse_writer(monkeypatch: pytest.MonkeyPatch, name: str) -> None:
|
||||
def _raise(*_a: Any, **_k: Any) -> None:
|
||||
raise _WriteRefused(f"{name} refused")
|
||||
|
||||
monkeypatch.setattr(run.outbox, name, _raise)
|
||||
|
||||
|
||||
def _budget_stop_select(blob: str, _role: str) -> str:
|
||||
"""T9's shape: the bought attempt's reply never parses, the retry exhausts the round ledger,
|
||||
and ``BudgetExceeded`` leaves ``run_project`` as an exception with BOTH ``finally`` writers
|
||||
armed (parse failures exist, and a reviewer was given)."""
|
||||
if _GENERATION_MARK not in blob:
|
||||
return "ok"
|
||||
return "not json at all" if _FEEDBACK_SENTINEL in blob else _run_reply("Belysning", 30_000)
|
||||
|
||||
|
||||
async def test_a_write_error_never_displaces_the_stop_that_was_already_in_flight(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""The review writer runs from a ``finally`` on EVERY reviewed run, so an ``OSError`` from it
|
||||
lands while ``BudgetExceeded`` (or ``ProposalReviewInputError``) is propagating — and a bare
|
||||
call there REPLACES the in-flight exception. Neither ``except ProposalReviewInputError`` nor
|
||||
the CLI's refusal tuple catches ``OSError``, so the operator got a traceback instead of the
|
||||
``run stopped:`` line, and the reason the run ended was gone.
|
||||
|
||||
Detach point: dropping the ``in_flight`` guard around the writers. The stop must survive, and
|
||||
the write failure must still be SAID — swallowed silently, a missing artefact would look like
|
||||
a run that had nothing to record (T10/T11's whole distinction)."""
|
||||
_refuse_writer(monkeypatch, "write_proposal_reviews")
|
||||
|
||||
with pytest.raises(BudgetExceeded):
|
||||
await _run_with(
|
||||
outbox_dir=tmp_path / "outbox",
|
||||
reviewer=_RunReviewer(),
|
||||
select=_budget_stop_select,
|
||||
max_rounds=2,
|
||||
)
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert f"{_RUN_ID}-proposal-reviews.json" in err
|
||||
assert "write_proposal_reviews refused" in err
|
||||
|
||||
|
||||
async def test_the_parse_failure_writer_carries_the_same_guard(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""The SIBLING writer in the same ``finally``. It has the identical shape and runs only when a
|
||||
parse failed — rarer, never absent — so it gets the same guard and its own witness: one
|
||||
helper, two call sites, and a change to only one of them is what a shared seam exists to
|
||||
prevent (kø-(p))."""
|
||||
_refuse_writer(monkeypatch, "write_parse_failures")
|
||||
|
||||
with pytest.raises(BudgetExceeded):
|
||||
await _run_with(
|
||||
outbox_dir=tmp_path / "outbox",
|
||||
reviewer=_RunReviewer(),
|
||||
select=_budget_stop_select,
|
||||
max_rounds=2,
|
||||
)
|
||||
|
||||
assert f"{_RUN_ID}-parse-failures.json" in capsys.readouterr().err
|
||||
|
||||
|
||||
async def test_a_write_error_with_nothing_in_flight_is_still_an_error(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The CONTROL, and the reason the guard is conditional rather than a blanket ``except``. On a
|
||||
run that otherwise SUCCEEDED there is no stop reason to protect, so a disk that refused the
|
||||
artefact must reach the caller: 'the outbox is unwritable' silently downgraded to a clean
|
||||
return is how a run reports success it cannot evidence.
|
||||
|
||||
Without this arm the guard could be mutated to swallow unconditionally and stay green."""
|
||||
_refuse_writer(monkeypatch, "write_proposal_reviews")
|
||||
|
||||
with pytest.raises(_WriteRefused):
|
||||
await _run_with(outbox_dir=tmp_path / "outbox", reviewer=_RunReviewer())
|
||||
|
||||
|
||||
async def test_t10_a_reviewer_nobody_could_consult_still_writes_an_empty_record(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue