Repository navigation
[Bug]: Flaky browser and server tests on 4-vCPU CI runners, and a Windows-only update.test.ts failure #22324
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
Build, CI, or release tooling
Steps to reproduce
My fork (Alb11747/t3code) runs upstream's release checks on GitHub-hosted 4-vCPU ubuntu-24.04 runners. The same checks also run in Alb11747-Labs/t3code on Blacksmith and on a self-hosted 4-vCPU Linux runner. I went through every failed check job in both repositories from October 9 to 20, and separated intermittent failures in upstream tests from failures caused by fork patches or runner setup. Several of the flaky tests are already fixed on main; the ones below are not. One more test fails on Windows every time (item 3).
1. apps/web/src/components/chat/ComposerBannerStack.browser.test.tsx › "keeps only the usage limit attached while an active goal waits in the collapsible stack" (still on main at 67c0fc0cdaf9)
There is no deterministic repro. It failed in 2 of the runs I looked at, and the same commit passed on rerun:
- run 39397442136, attempt 2 failed, then attempt 3 passed. Both checked out the same commit.
- run 39357774973 failed with the same assertion.
- Upstream CI: run 39430065694 (PR fix(web): preserve citations when adding review comments #19955) failed at the same line,
ComposerBannerStack.browser.test.tsx:97, with the test file identical tomain.
vp run --filter @t3tools/web test runs it. I found no open PR that changes this. #21901 parks the pointer to fix a similar flake in RightPanelTabs, which supports the pointer explanation below.
2. packages/ssh/src/tunnel.test.ts › "launch script › replaces a managed server that exited but was not reaped" (not fixed by #15715; fix pending in #20052)
It failed twice on my fork (39056613670, 39358898306) while setting up the zombie process, before any launcher behavior runs. #15715 (e6765094cb6b) made the test poll ps for the Z state. That doesn't fix it. The fixture prints the child's pid before sh reaches exec. If the kill lands in that window, dash reaps the child on SIGCHLD, so there is no zombie to find, and the poll exits on the first empty ps.
Running the fixture command standalone on Linux (dash), with an immediate SIGKILL, the old single check failed 92 of 600 times. The current polling check failed 75 of 600 times. In every failure the child's /proc entry was gone. Upstream CI hit it after #15715 too: run 39378445705 failed at tunnel.test.ts:1472, the new polling assertion. Open PR #20052 fixes the cause by killing the child only after sh has exec'd into node.
3. apps/server/src/cli/update.test.ts › "finds the launcher a bare command name resolved to on PATH" fails on Windows every time (not flaky; still on main)
The test forces HostProcess.Platform to linux and joins PATH entries with : (update.test.ts:147, :165), but it builds native paths. On Windows, the C: drive prefix is split off as a PATH entry, so the resolved launcher loses its drive letter. On Windows 11 x64 with Node 24.21.0, vp test run apps/server/src/cli/update.test.ts at 67c0fc0cdaf9 gives 1 failed, 8 passed, and 1 skipped. The failure is at update.test.ts:162, where the expected path starts with C:\Users\... and the received path starts with \Users\.... My closed #20210 fixed it by using the host platform and its PATH delimiter in that test only.
4. packages/provider-opencode/src/server/OpenCodeServerLedger.test.ts › "observing as linux › records a group whose spawned wrapper already exited" (fix pending in #22209)
expected true to be false at line 112. It failed on 2 of 10 runs on the self-hosted runner, and each time the same commit passed on rerun (39415050336, 39434300590). Upstream CI hit it too: 39387268551 attempt 1 failed and attempt 2 passed, at the same main commit. This is the race #22209 describes: the ledger records a forked child's command before it execs, then skips stopping it because the command changed. I reproduced it on Linux with #22209's gated test: the unchanged ledger failed 3 of 3 runs, and #22209's ledger passed 3 of 3. Only the test is flaky, but the race is in the ledger itself.
5. apps/web/src/components/chat/MessagesTimeline.browser.test.tsx › "MessagesTimeline scroll position › restores a manual reading position after reopening and switching threads" (still on main; test-only fix below)
expected 600 to be less than or equal to 1 at the final switch back to the saved thread, off by exactly the wheel distance. It failed on Blacksmith (39389586580; the rerun at the same commit passed) and on the self-hosted runner (39386750832; the same commit passed in another run). #21968 also reports this test failing 2 of 3 times on unmodified main, but doesn't fix it.
The cause is the test fixture, not the component. After reopening, the test renders MessagesTimeline directly, without the live-follow wiring that ChatView passes (liveFollowEnabled plus an onManualNavigation callback that turns it off). Live follow stays on. Once restoration completes, it can save the position as atEnd: true over the stored manual offset, and the final switch back then lands 600 px off. I reproduced that overwrite on Linux. Rendering the reopened timeline through the same Chat wrapper the first half of the test uses fixed it, without touching the component. All 80 tests in the file passed, and the target test passed 10 repeats.
6. apps/web/src/components/settings/ProjectFaviconPickerDialog.browser.test.tsx › "loads previews asynchronously and only acquires offscreen images after scrolling" (still on main, after #22174)
The final expectPreview(last) timed out with no image: received { width: undefined, fallback: true }, expected { width: 8, fallback: false }. At the same commit, one attempt passed and the next failed. It failed again at a later commit (job 118747481738), making 3 failures on the self-hosted 4-vCPU runner, and passed on rerun. #22174 fixed an earlier race in the same assertion, and that fix was already in the failing source, so this is a different failure. expectPreview polls with Vitest's default 1-second timeout, and each of those failures ended with no <img> in the row. Slowness isn't the cause, though. When I replaced the poll with explicit waits for the <img> to render and decode, the test hung until the 15-second test timeout (run 39470879803), so some step never happens at all. On its own the file passed 30 of 30 runs on the same runner, including under CPU stress. It has only failed in full web suite runs, which suggests leftover page state from an earlier browser test file, as in item 8.
7. apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts › "queues a plain send behind the stopped queue and releases its hold" (lower confidence)
Expected queued texts ['queued', 'sent'] and received ['sent'] (39373218653). The test assumes the provider's stop is still in flight, but the fake interruptTurn returns immediately and the test drains effects before reading the queue, so the queued message can already have moved on. It failed on fork code that changes queue delivery, so I haven't reproduced it on upstream's code alone. The same unsynchronized assumption is in upstream's test (lines 878–935). Holding interruptTurn on a Deferred until the queue has been checked removes it.
8. apps/web/src/components/RightPanelTabs.keyboard.browser.test.tsx › "right panel new-tab shortcut › uses a custom binding from a text field and closes on Escape" (fix pending in #21901)
Focus ended on <body> instead of the trigger button (run 39470879803). This is the failure #21901 describes: the real browser pointer stays wherever an earlier test file left it, and if the menu opens under it, Escape triggers a hover close that skips returning focus. #21901 parks the pointer before each test.
Expected behavior
These tests pass consistently on a fixed commit and on every supported platform.
Actual behavior
ComposerBannerStack fails at the first not.toBeVisible() check on the goal banner, before anything clicks the stack:
Error: expect(element).not.toBeVisible()
Received element is visible
Caused by: Error: Matcher did not succeed in time.
The visibleBanners() check just before it passed, so the stack started collapsed and then expanded during the retry window. The stack expands on pointer enter (ComposerBannerStack.tsx:127), and the test never moves the pointer away from where the fixture renders. My guess is that the pointer left over from an earlier test triggers that pointer-enter, but I haven't confirmed it.
The SSH test failed with AssertionError: expected '' to include 'Z'.
Impact
Minor bug or occasional failure
Release checks fail and need a rerun.
Version or commit
ComposerBannerStack: still present on upstream main at 67c0fc0cdaf963b103518862dd7b68a9c758ecf2. The failing runs checked fork commits based on upstream nightly v0.1.1-nightly.20261020.3386.
Environment
GitHub Actions ubuntu-24.04 (4 vCPU), Blacksmith Ubuntu 24.04, and a self-hosted 4-vCPU Linux runner in a container. All use Node 24.21–24.22, Vite+ 1.0.0, and headless Chromium. Upstream's checks use 8-vCPU runners. I haven't compared 4 and 8 cores, so the core count is the environment, not a proven cause. I excluded failures caused by the self-hosted runner's setup, such as a missing font and out-of-memory kills.
Logs or stack traces
See the run links above.
These flaky tests from the same period are already fixed on main, so I'm only listing them for reference:
AcpRegistryAdapterV2.test.tsmode-picker tests: replay status file read before it was written. The same commit failed and then passed. My closed test(server): ACP replay tests no longer read an empty status file #20113 proposed a fix, and fix(acp): keep model discovery and selection on the same option #19567 fixed it.WorkflowOpeningBlock.browser.test.tsx: waited for the real Shiki highlighter. Fixed by test(web): fix the workflow script test that times out on busy CI runners #21699.StyledDiffCodeView.browser.test.tsxworker lifecycle: fake timers also advanced the worker's startup deadline. Fixed by test(web): isolate diff worker idle clock #22259.MessagesTimeline.test.tsx:beforeAllimport timed out at 30 s. The file was removed by the browser migration in test(web): chat markdown and timeline tests run in a real browser #20905.
Workaround
Rerun the failed job.
Investigated by GPT-6.1 Sol (Codex) and reviewed by Claude Opus 5.5 (Claude Code), both in T3 Code, from the fork's CI logs, upstream's CI logs, and source. New runs for this report: the Windows update.test.ts run and the standalone Linux fixture stress test described above.
Activity
[-][Bug]: ComposerBannerStack browser test fails intermittently on 4-vCPU CI (plus flaky-test audit)[/-][+][Bug]: Flaky ComposerBannerStack browser test on 4-vCPU CI, and a Windows-only update.test.ts failure[/+][-][Bug]: Flaky ComposerBannerStack browser test on 4-vCPU CI, and a Windows-only update.test.ts failure[/-][+][Bug]: Flaky browser and server tests on 4-vCPU CI runners, and a Windows-only update.test.ts failure[/+]tris203 commentedon Oct 20, 2026
Note
Claude Opus 5.5 responding on behalf of @tris203
Status of each item against
mainat5bcc90e1116, plus the open PRs:ComposerBannerStackusage limit stack#22523 (open)
tunnel.test.tszombie reapupdate.test.tslauncher PATH on WindowsOpenCodeServerLedgerexited wrapper#22523
MessagesTimelinemanual reading position restoreProjectFaviconPickerDialogoffscreen previewsSteeringCompletionplain send behind stopped queueRightPanelTabskeyboard Escape focus#22523
#22524 (merged) gives the release web tests their own runner, matching CI. That should make the browser flakes rarer in release runs, but it doesn't remove the races behind 5 and 6.
The new PRs and findings are test-only. Each one was reproduced before the fix and passes afterwards:
HostProcess.Platformtolinux, soresolveLauncherPathsplits PATH on:. The test builds that PATH from native paths. On Windows,C:\Users\…\missing:C:\Users\…\binsplits intoC,\Users\…\missing,Cand\Users\…\bin. The last entry has no drive letter but still exists, since Windows resolves it against C:, so the function returns the\Users\…path. A probe that runs the realresolveLauncherPathwithpath.win32gave exactly that value. The product isn't affected:Platformdefaults to the host platform, and nothing outside tests overrides it. Forcinglinuxcame from the neighbouring symlink tests, which do need it. This test only uses the platform to pick the PATH delimiter, so the fix is to use the host platform and join PATH with its delimiter, the same logic as #20210.Chatwrapper doesn't fix it. That wrapper turns off live follow only on a wheel event, and nothing scrolls the wheel after the reopen. The reopened timeline still gets a no-oponManualNavigation, so live follow stays on. The save that runs after the restore then marks the positionatEnd: true. The fix passes the sameonManualNavigationwiring thatChatViewuses.ChatViewitself isn't affected.setTimeout(0). React's subscribing effect only runs after the next frame. If the test publishes the URL in that gap, the value is removed along with the atom, and the row stays on the fallback icon forever. That's why the explicit waits hung. The fix creates the fixture atoms withAtom.keepAlive. We think, but haven't confirmed, that the full suite's load widens the gap.firstbecomesinterrupted, a reactor fiber started withforkDetach(Orchestrator.tsrun.updated→startNextQueuedRun) movesqueuedforward, anddraindoesn't wait for that fiber. If the fiber runs before the test reads the projection, the result is['sent']. The fix holds the fakeinterruptTurnon aDeferreduntil the queue has been checked, as you suggested.t3dotgg commentedon Oct 20, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Theo
Update after #22523 merged (f9dd259):
ComposerBannerStackrender()tunnel.test.tsupdate.test.tson WindowsOpenCodeServerLedgertrack(). That hides the pre-exec race; it does not fix it. #22209 fixes the ledger and is still needed. Its gated test should replace the fixture waitMessagesTimelinerestoreProjectFaviconPickerDialogAtom.keepAlivechange. #22530 closed as a duplicateSteeringCompletionRightPanelTabs#22523 also fixed the
ClaudeAdapterV2steer hang,transferBudgetV2,AcpAdapterV2detached PIDs, and the diff-worker timeouts inStyledDiffCodeViewandPullRequestDetailPanel. This issue should stay open until 3, 4, 5 and 7 land.