Bound each browser probe's own answer so a wedged gate names the probe - #100
Conversation
`page.evaluate` takes no timeout in any binding. `render_checks.py` already races the probe module's load against `call.timeoutMs`, but the probe it then invokes was awaited with nothing bounding it — and two probes await a promise the page supplies rather than a fact it states: `nextFrame` waits on a rendering turn, and `retiredSlots` on each holder's animations. A page whose compositor has stopped drawing settles neither, so the reading ran for as long as the process lived, with no probe named and nothing printed. Race the answer against the same deadline the load already states. The rejection comes back through `evaluate`, and `render_version` already turns a `PlaywrightError` from its probe module into a gate finding, so a probe that stops now says which one stopped and the bound it passed.
|
Each passed in the attempt the other failed in. Neither test calls What the two attempts do say is the thing this PR is for: both ran the whole suite to a reported result — 24:56 and 21:30 — rather than spending the 45-minute step bound and naming nothing. I have not opened a fix for either failure: they are separate concerns from this diff, and |
PR #100 bounded probe promises so CI named a stopped probe, but the operations and their deadlines still lived inside the page. This change removes the underlying waits: shipped probes publish synchronous readings or readiness facts, module and frame progress is observed through Playwright's bounded driver waits, and redundant per-holder animation waits are gone. It also closes the two lifecycle races exposed by PR #100's own CI: complete-revision waits now include the server reading applied to that document, and response-transforming route handlers are drained before their page is disposed. The remaining image-event wait follows the same publish-and-poll pattern. Tests: `uv run pytest tests --run-nightly -q` (1484 passed, 1 skipped); `uv run pre-commit run --all-files`. > _This was written by Codex on behalf of max-sixty_
Problem
Run 33220366482 (
db3f5d0) spent the step's whole 45 minutes and left no failure to read. Under the-v#81 added, both workers name what stopped them, and they stopped in two different places.gw0printedtests/test_render_widgets.py::test_accept_all_decides_every_pending_suggestionat 23:48:35 — that is theResponse.jsonshape #85 diagnosed, and #85 has since landed onmainas e626f53.gw1printedtests/test_smoke.py::test_a_shipped_page_passes_the_real_browser_gateat 23:49:53, at 99%, and never reported it. That second one is left over: it is one line —render_gate_model.render_version(...)— so the wait is on the gate's own path, in shipped code rather than intests/, and it has now stopped three runs.page.evaluatetakes no timeout in any binding.render_checks.pyalready races the probe module's load againstcall.timeoutMs; the probe it then invokes was awaited with nothing bounding it. Two probes await a promise the page supplies rather than a fact it states, and both are reached that way:nextFrameisnew Promise(requestAnimationFrame), awaited atreadings.py:188as the last thing each scheme reads.requestAnimationFramestops in a page the compositor has stopped drawing.retiredSlotsawaitsPromise.allSettledover each holder's animations —a.finished, the same promisebb8b7e0had to bound on the test side.They are the only two: no other export under
render-checks/contains anawaitor a.then. #84 namednextFrameand scoped it out as product rather than suite behaviour, which is right — it belongs here rather than intests/render_harness.py, and this PR does not touch anything #84 does.Solution
Race each probe's answer against the deadline
_INVOKE_PROBEalready carries for the load, in the one place every probe goes through. Nothing new is declared:call.timeoutMsis the gate'sserved_timeout_ms, defaulting toSERVED_TIMEOUT_MS, which is Playwright's own 30 seconds — so a probe now runs out where every other browser wait in the gate and the suite runs out. The rejection comes back throughevaluate, andrender_versionalready turns aPlaywrightErrorfrom its probe module into a gate finding, soversion check --renderreports which probe stopped and the bound it passed instead of hanging.Bounding at
_INVOKE_PROBErather than at the two probes is what keeps the layer open: a probe added later that awaits the page is bounded by existing, and the suite's ownevaluate_probecallers are covered by the same edit.tests/CLAUDE.md's bounded-await rule is corrected to say so, since it currently names only the loader and would send an author to write a second race around a call that already has one.This does not claim to bound every unbounded await in the payload — only the ones reached through
evaluate_probe. Anawaitwritten directly into apage.evaluateargument elsewhere is still a wait nothing bounds.Testing
test_a_probe_that_never_answers_is_a_gate_finding, beside the loader's own deadline test. It serves the real probe facade withnextFramereplaced by a promise that never settles, runs the whole gate, and asserts the finding names the probe and the bound. The replacement holds its own resolver, asrequestAnimationFrameholds the callback it was handed — the first version let it be collected, and the driver ended the wait itself withResulting promise was garbage collected, which is a different arrangement and not the one that stopped these runs.Bug-back on this runner, same test both ways:
On the rebased tree (
b25a9e0), every suite file that reachesevaluate_probe, plus the one the wedge landed on:test_render_gate.pyandtest_smoke.py(66 passed),test_render_pages.py,test_render_startup.pyandtest_render_options.py(135 passed),test_render_widgets.py(61 passed), andtest_render_export.pywithtest_render_projection.py(97 passed, run against6045970before the rebase, along withtest_render_commands.py). The everyday suiteuv run pytest testsis 690 passed, 6 skipped, andpre-commit runover the three changed files is clean. The full--run-nightlysuite has not been run here; CI runs it on this branch.The three runs this wait stopped
Each row is the nodeid a worker printed and never reported, found by pairing
-v's start lines against its[gwN] … PASSED/FAILEDlines over the whole job log:16a94ad(#95's branch)test_smoke.py::test_a_shipped_page_passes_the_real_browser_gate, andtest_render_options.py::test_a_question_owns_one_thread_in_the_page_and_panel1c5edadtest_smoke.py::test_a_shipped_page_passes_the_real_browser_gate, andtest_render_conversations.py::test_a_resolved_thread_can_be_reopeneddb3f5d0test_smoke.py::test_a_shipped_page_passes_the_real_browser_gate, andtest_render_widgets.py::test_accept_all_decides_every_pending_suggestionThe second nodeid on each row is #85's shape rather than this one — all three reach
Trafficthroughround_trip. This PR does not claim them; e626f53 does.That first row is attempt 1 because its failed jobs were re-run afterwards, and the re-run's log is what
gh run view --jobreturns now; attempt 1's own log is still readable throughactions/jobs/98844279758/logs, and that is the one parsed here.The diagnosis on #95 named both of this PR's awaits after the first of these three runs, but #95 was a test-side fix and could not carry them. That comment is now on a closed PR, so this restates it rather than linking to it as if it were pending.
Automated fix for failed run