You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
tests/test_tui_app.py::test_poll_skips_while_another_holds_the_lock fails
intermittently on Windows CI with textual.worker.WorkerCancelled: Worker was cancelled, and did not complete.
Where it was seen
Run 31743771689,
job test (windows, py3.11) — the only failure in the job (1 failed, 5258 passed, 173 skipped, 5 xfailed), after 10m22s.
test (windows, py3.14) in the same run, same commit passed this test.
Re-running that one job on the same commit passed (12m22s). Recorded as evidence
of nondeterminism, not as a remedy — the zero-retry policy in docs/testing.md is the reason this is an issue and not a rerun loop.
DashboardScreen._poll is declared @work(thread=True, exclusive=True, group="poll")
(src/bmad_loop/tui/screens/dashboard.py:746), and the screen arms a periodic tick with self.set_interval(1.0, self._tick) (dashboard.py:292). exclusive=True means a newer worker in group poll cancels the older one.
The test starts its own poll worker by hand and then awaits it:
If the 1s interval tick fires between those two lines and kicks its own poll, Textual
cancels the test's worker, and Worker.wait() raises WorkerCancelled for a
CANCELLED worker. The test's preamble is three await until(...) spins plus a
non-blocking lock-acquire spin, so where it lands relative to the 1s boundary is luck —
and CI runners are slow enough (this job ran 10m22s) to straddle it.
Note what this does not mean: the property under test still held. The guarded body
never ran either way, which is exactly what assert ctx.entries == before checks. The failure is the wait() call conflating
"cancelled by the interval tick" with "did not complete" — a test-harness defect, not a
product one.
Suggested direction (not prescriptive)
Either stop the interval before the hand-started worker (screen's timer paused for the
duration), or treat WorkerCancelled as an acceptable outcome and assert the invariant
(ctx.entries == before) directly rather than through wait(). The second keeps the
regression this test was written for — two threads feeding ctx.log's pyte stream —
while dropping the dependence on winning a race against the tick.
Whichever is chosen, the ablation rule applies: the test asserts an absence (the
guarded body never ran), so delete the _poll_lock guard and confirm it reddens before
trusting the repaired version.
Housekeeping
docs/testing.md (§ "CI, flakes, and deliberate absences") enumerates the open flake
instances inline — "#360 and #529 are the open instances". That line needs this issue
added when it is next touched. It is already stale on the other half (#529 is closed),
which #553 tracks.
The test itself arrived with e325764"fix(tui): serialize poll worker and anchor log
jumps" — it is the regression guard for #178. The guard is worth keeping; only the way
it awaits is at fault.
Filed rather than fixed there — that PR is atomic_write_text + surrogate
neutralization on the deferred-work ledger writers and must not widen into the TUI.
tests/test_tui_app.py::test_poll_skips_while_another_holds_the_lockfailsintermittently on Windows CI with
textual.worker.WorkerCancelled: Worker was cancelled, and did not complete.Where it was seen
job
test (windows, py3.11)— the only failure in the job (1 failed, 5258 passed, 173 skipped, 5 xfailed), after 10m22s.test (windows, py3.14)in the same run, same commit passed this test.of nondeterminism, not as a remedy — the zero-retry policy in
docs/testing.mdis the reason this is an issue and not a rerun loop.deferredwork.py,engine.py,sweep.pyand
platform_util.pyand no TUI code at all —git diffoversrc/bmad_loop/tui/andtests/test_tui_app.pyis empty for that branch.Why it races
DashboardScreen._pollis declared@work(thread=True, exclusive=True, group="poll")(
src/bmad_loop/tui/screens/dashboard.py:746), and the screen arms a periodic tick withself.set_interval(1.0, self._tick)(dashboard.py:292).exclusive=Truemeans anewer worker in group
pollcancels the older one.The test starts its own poll worker by hand and then awaits it:
If the 1s interval tick fires between those two lines and kicks its own poll, Textual
cancels the test's worker, and
Worker.wait()raisesWorkerCancelledfor aCANCELLED worker. The test's preamble is three
await until(...)spins plus anon-blocking lock-acquire spin, so where it lands relative to the 1s boundary is luck —
and CI runners are slow enough (this job ran 10m22s) to straddle it.
Note what this does not mean: the property under test still held. The guarded body
never ran either way, which is exactly what
assert ctx.entries == beforechecks. The failure is thewait()call conflating"cancelled by the interval tick" with "did not complete" — a test-harness defect, not a
product one.
Suggested direction (not prescriptive)
Either stop the interval before the hand-started worker (
screen's timer paused for theduration), or treat
WorkerCancelledas an acceptable outcome and assert the invariant(
ctx.entries == before) directly rather than throughwait(). The second keeps theregression this test was written for — two threads feeding
ctx.log's pyte stream —while dropping the dependence on winning a race against the tick.
Whichever is chosen, the ablation rule applies: the test asserts an absence (the
guarded body never ran), so delete the
_poll_lockguard and confirm it reddens beforetrusting the repaired version.
Housekeeping
docs/testing.md(§ "CI, flakes, and deliberate absences") enumerates the open flakeinstances inline — "#360 and #529 are the open instances". That line needs this issue
added when it is next touched. It is already stale on the other half (#529 is closed),
which #553 tracks.
The test itself arrived with
e325764"fix(tui): serialize poll worker and anchor logjumps" — it is the regression guard for #178. The guard is worth keeping; only the way
it awaits is at fault.
Out of scope for #580
Filed rather than fixed there — that PR is
atomic_write_text+ surrogateneutralization on the deferred-work ledger writers and must not widen into the TUI.