test(portfolio): gate the wave handler's catch width and the failed-project ledger (v/t/s)
Three items on one seam — what a FAILED project does to the wave loop — plus the snapshot copy they sit next to. (v) The catch is BaseException, not Exception, and that width was ungated. The existing collect-and-continue test raises RuntimeError, so it stays green when the handler is narrowed: measured, the whole of test_portfolio_concurrent_ loadbearing.py (13 tests) passes under the narrowing. asyncio.CancelledError is the one realistic vector that separates the two — probed first, gather( return_exceptions=True) COLLECTS it, while KeyboardInterrupt propagates regardless and could never be helped by a wider catch. Narrowed, a cancelled member is cast into runs as a fake RunResult and the pass dies in _aggregate, pointing away from its cause. RED measured. (t) sum_token_usage excludes a failed project's spend, and that is the honest answer, not a bug: a run that died before producing a stamp has no provenance, and inventing one is the fabrication RunFailure exists to avoid. What needed gating is that those tokens still reach the ledger the global cap is enforced against — otherwise a repeatedly-failing project burns budget while the meter reads clean. Pins meter.spent as the pass's real cost, sum_token_usage as the completed-run subtotal, and their difference as exactly the failed spend. RED measured against the likely "fix" (sourcing sum_token_usage from the meter), which is wrong because a seeded meter also carries EARLIER passes' spend; 21 existing budget/portfolio tests stay green under it. (s) _wave_snapshot uses dataclasses.replace, so a field added later is carried without touching the function. Not cosmetic: measured, dropping retriever by hand-enumerating left all 585 tests green — the Step-2 coverage its docstring credited no longer existed, so the S3.1 retriever seam could be downgraded mid-pass in silence. Now gated by a property test derived from dataclasses.fields (not a field count, the shape rejected earlier). The explicit verdicts copy is retained and separately gated: replace(store) alone shares the caller's list and takes the byte-identical determinism test RED. strict=True on the zip is documented as deliberately untested — measured green when dropped, since gather is built from exactly snapshots, so a test could only go red by manufacturing a mismatch and would exercise zip rather than this pass. The new double is registered in the S2.5 consolidation guard's delegating- overrides list rather than the guard being weakened; it already delegates via super()._inner_get_response, which test_delegating_overrides_call_super now enforces on it. 583 -> 586 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbgTCEZma764i1rHTrzceU
This commit is contained in:
parent
8910a673ea
commit
873f5fa272
4 changed files with 322 additions and 8 deletions
|
|
@ -31,6 +31,10 @@ _DELEGATING_OVERRIDES = [
|
|||
# S3.3 ordering probe: yields to the event loop N times, then delegates. It cannot live in the
|
||||
# reply-selector seam, which the canonical calls synchronously and so can never await.
|
||||
"tests/test_portfolio_concurrent_loadbearing.py",
|
||||
# S3.3 failure-accounting probe: RAISES for one project (after N completed calls), otherwise
|
||||
# delegates. Like the ordering probe it cannot live in the reply-selector seam — that seam
|
||||
# returns a reply string, and this double's whole subject is the absence of one.
|
||||
"tests/test_portfolio_failure_accounting_loadbearing.py",
|
||||
]
|
||||
|
||||
# Doubles in a DIFFERENT lineage (``spikes._harness.FakeChatClient``). There is no canonical
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue