Skip to content

Wait for every blanket-answer round trip before asserting the control is hidden - #160

Merged
max-sixty merged 1 commit into
mainfrom
fix/ci-33345068975
Aug 31, 2026
Merged

Wait for every blanket-answer round trip before asserting the control is hidden#160
max-sixty merged 1 commit into
mainfrom
fix/ci-33345068975

Conversation

@leaf-agent

Copy link
Copy Markdown
Collaborator

Problem

ci on 72be4be failed with 35 nightly tests red. Rerunning all 35 node ids against c0a5b65 leaves four: the catalog digest (#156), the two page-map chord reads (#157), and this one, which nothing owns.

FAILED tests/test_render_controls.py::test_a_wide_banner_spends_status_copy_before_action_reach
AssertionError: Locator expected to be hidden
  6 × resolved to <button ... class="lf-btn lf-answer-all ...">✓ Accept all (3)</button>
  3 × resolved to ... ✓ Accept all (2)
  4 × resolved to ... ✓ Accept all (1)

The count is walking down and running out of budget. The blanket answer decides its decisions one at a timeawait source[verb]?.() per decision — so the press owes one round trip per decision, and the test released the held first answer straight into expect(answer_all).to_be_hidden(). That single 5s budget was covering three sequential server trips, not the repaint it is for. tests/CLAUDE.md names this exactly: "Letting expect absorb the page's next read hides which mechanism supplied the wait and spends its timeout budget on transport rather than on the assertion."

Instrumenting the sequence on an idle runner gives event → state → event → state → event, 1.89s end to end. Three trips at ~0.6s each fit inside 5s on an idle machine and do not on a loaded one, which is why this is nightly-and-under-load only.

test_accept_all_decides_every_pending_suggestion drives the same control and does not have the bug: it waits for each widget's own settle first, so its to_be_hidden only has to cover the last repaint, and its comment says why. This site was the one that skipped the staging.

Solution

State the transport with _until on the Traffic counters — the same causal helper this test already uses eight lines above to hold the answer in the wire — then let to_be_hidden assert only the repaint it is about. The number of trips owed is read off the control's own face (✓ Accept all (3)) rather than written out, so the fixture's arithmetic stays in one place, and an owed > 1 guard fails loudly if a fixture change ever leaves this test a single trip and stops it exercising the sequence at all.

This is not a raised timeout. _until is bounded at 30s with the counters in its failure message, so a genuinely stuck answer now fails naming the missing evidence instead of pointing at a control that "never went".

Testing

Playwright driving real Chromium on this runner, at 8683b9b:

  • uv run pytest tests740 passed, 6 skipped. This is the gate ci runs on the PR; the test fixed here is nightly-only and not exercised by it.
  • uv run pytest tests/test_render_controls.py --run-nightly -n464 passed, three times consecutively on an otherwise-idle machine.
  • ruff check and ruff format --check on the changed file, at the v0.16.1 .pre-commit-config.yaml pin — clean.
Reproducing the failure, and the mutation checks

The test passes 6/6 at -n0 on c0a5b65 unmodified, so -n0 alone leaves it unclassified. Stalling the driver for 300ms between held[0].continue_() and the hide assertion reproduces the CI shape exactly on the unfixed test — the button walks (3) → (2) → (1) and the expect runs out:

5 × ... ✓ Accept all (3)
5 × ... ✓ Accept all (2)
3 × ... ✓ Accept all (1)

With this change in place the same stall passes. Two mutations confirm the new wait bites rather than just returning:

mutation result
traffic.sends == owedowed + 1 fails: the page never settled every one of the 3 answers the blanket press owed: the wait began on sends=1 acked=0 pending=1 asked=3 heard=3 and gave up on sends=3 acked=3 pending=0 asked=6 heard=6 — and this also confirms owed reads 3 and that the predicate's own terms are what carry it
driver stall re-applied to the fixed test passes in 8.55s

Timed trace of the released sequence on an idle runner, seconds from continue_():

0.38 FIN event    0.66 FIN state
1.17 FIN event    1.38 FIN state
1.81 FIN event    → hidden at 1.89

The other .lf-answer-all sites were checked for the same shape and do not have it: test_render_gate.py navigates away, test_render_startup.py puts wait_for_function(BOTH_STAMPS) between the release and the hide assertion and has one decision rather than three, and test_render_projection.py asserts the control is absent.


Automated fix for failed run

… is hidden

test_a_wide_banner_spends_status_copy_before_action_reach released the held
blanket answer and asserted the control hidden in one expect. The press decides
its decisions one at a time, so that budget was covering three sequential
server round trips rather than the repaint it is for; under load the third
answer lands after it and the control is still reading its count.

Name the transport with _until on the outbox counters, taking the number of
trips owed from the control's own face.
@max-sixty
max-sixty merged commit 6bbfb4b into main Aug 31, 2026
3 checks passed
@max-sixty
max-sixty deleted the fix/ci-33345068975 branch August 31, 2026 05:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants