Skip to content

Two tests read a frame before the deferred scroll pass lands, and fail at random under load #53

Closed

Description

@bmethod

Reached by: run bun test src/ as the full suite under load (or in CI); the two named tests fail intermittently depending on whether the setTimeout(0) deferred pass lands before the test reads its frame.

What happens

Two tests fail intermittently on a full suite run and pass when their own file is run alone. Four sightings across one working session, two of them named:

  • the editor opens in view when the layout moves the row it edits (src/ui/settings-screen.test.tsx)
  • a lane-series timeout (src/store/history.test.ts)

Each failure was followed by a passing full run with no change to the tree.

The mechanism

Both read a rendered frame that is only correct after a deferred second pass has landed.

src/ui/settings-screen.tsx scrolls twice on purpose. The first pass decides where the selected row goes; the second, scheduled with setTimeout(..., 0), re-measures once the new layout has been drawn, because opening an editor collapses two columns into one and the row has not moved yet on the first pass. That comment is in the source and the behaviour is deliberate.

A setTimeout(0) is a macrotask. Under a full suite run the event loop has other work, so the second pass can land after the test reads its frame. Alone in its file, it lands first. Nothing about the code under test is wrong: the test reads a frame the program has not finished producing.

Why this matters more than a rerun

Three defects in this session were caught only because a control existed for a neighbouring path (see vanillagreencom/vsys#43 and vanillagreencom/vsys#50). A suite that goes red at random teaches a reader to rerun rather than to look, and the cost of that habit is paid the one time the red is real. It also blocks the merge queue for as long as it takes someone to notice the failure is not theirs.

Deliverable

The test harness waits for the deferred pass instead of racing it. src/test/harness.tsx already owns mounting and frame reading for every screen, so the wait belongs there rather than in each test: a frame read that yields to the macrotask queue before returning, or an explicit settle step the two affected tests call.

Whichever shape is chosen, the check is the same: run the full suite repeatedly under load and see both named tests stay green, then confirm each still fails when its own behaviour is planted.

Impact

Every developer running the suite, and every pull request whose continuous integration run happens to land on it. Observed four times in one session on one machine.

Activity

  1. linear-code commented on Sep 10, 2026

    @linear-code
  2. bmethod commented on Sep 10, 2026

    @bmethod
    CollaboratorAuthor

    Three more tests joined this class today, in src/ui/agent.tsx's scroll close on #50.

    They are deterministic as written, because the wait is explicit — the test asks for the settled state rather than reading a frame and hoping the deferred pass has landed, and the pane state settles on arrival instead of pressing keys inside the same tick. That is the shape this issue proposes for the two flaky ones.

    Worth recording anyway: it is more tests depending on a setTimeout(0) landing, not fewer. The count of tests in this family is now high enough that the harness-level fix proposed here is cheaper than continuing to get the wait right one test at a time.

    The reason the deferred pass cannot simply be removed, for whoever picks this up: a row that has just grown does not know its size until the layout after the render that grew it. Both screens read geometry after that layout on purpose. The fix is a harness that waits for it, never code that stops needing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions