Skip to content

test(desktop): stop four E2E specs racing scroll settles and focus - #8743

Open
matt2e wants to merge 1 commit into
mainfrom
flakey-tests-desktop-e2e-scroll-focus
Open

matt2e wants to merge 1 commit into
mainfrom
flakey-tests-desktop-e2e-scroll-focus

Conversation

@matt2e

@matt2e matt2e commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Units 2–5 of the 23–30 Oct desktop Playwright flake audit: four E2E specs that raced scroll settles and focus. All four fixes are test-only. This is one of six PRs split from the reviewed flakey-tests branch (siblings below).

navigation.spec.ts:472 "composer Buzz chip labels wrap without orphaning their icons"

11 jobs failed in the window, 6 of them after the earlier fixes, including 4 of the 7 main runs that carry every fix. Run 40970053997 (Smoke shard 7) failed all three attempts.

  • Mode A (jobs 122971493553, 123066839967, 123572467962, 123819180597 and retry 2 of 123784825042): fragmentStep is null at :634. Rows key on renderKey = localKey ?? id, and a head refetch or live echo after the ack drops the localKey. The row remounts and loses the 220 px width the test set earlier. Change: re-land the test half of closed fix(desktop): show channel members before joining #6852. Set the width and measure in one expect.poll, and set the width again before each fragment measurement.
  • Mode B (jobs 123573056202, 123784199542, 123784825042 twice, 123804428893, 123818728963, 123819543461, and 122924164125): the tooltip-centre poll at :667 receives 81.35 or Infinity. Radix Tooltip closes when an ancestor of its trigger scrolls. In the failing attempt, the post-send settle took its last step 21 ms or less before focus. Change: wait for the sent row's message-send-status to clear. Then poll until the timeline sits at its floor and holds there for two animation frames, and only then focus the chip. positionOverFragment asserts data-state is open. There is no retry loop around the race.

huddle-transcription.spec.ts:637 "animates the responding agent with the shared speaker ring"

scroll-history.spec.ts:1289 "fast middle-page scroll settles with continuous mounted coverage"

  • Runs: 9 jobs. After the fixes: main 40980404335 (job 123819180518) and two attempts on Wait for agent inventory updates in the desktop test #8720. The viewportCoverage poll expects < 100 and receives 200, the bottom spacer.
  • Cause: the bottom-settle intent armed when the channel first positions survives the prepend and the spec's scrollTop writes. Virtua's re-measure then pulls the list back to the floor.
  • Change: hover the timeline and wheel up 120 px first, since reader input cancels the intent. Then poll until the timeline is more than 32 px from the floor and steady for two frames.
  • The commit message calls the cancelling listener listenForReaderScroll, a name introduced by sibling fix(desktop): hold a deep-linked message against bottom settles #8742. On main, the same wheel listener is inline in useVirtualizedBottomSettle, so this fix doesn't depend on that PR.

custom-emoji.spec.ts:370 "reacting with a custom emoji renders via the loopback media proxy"

  • Runs: main 40787609859 and 40983029157 (Smoke shard 3), and BUZZ-271: Bound live-session revalidation #8241. The inline "Add reaction" opacity reads "1", not "0".
  • Cause: the non-modal Radix Popover returns focus to its trigger in a setTimeout(0) after its 100 ms exit animation. That can land after the spec's blur.
  • Change: wait for the "Open reactions" trigger to be focused, then blur.

Related issue

None found. Sibling #8744 also edits navigation.spec.ts, in a separate hunk. The two merge cleanly in either order. #8622 adds an unrelated test at the end of navigation.spec.ts, and #7812 (stale since 10-06) adds tests elsewhere in three of these files.

Testing

Not run locally, because Playwright bogs this machine down. Flake Check will repeat the touched specs. pnpm typecheck and pnpm check pass. An ad hoc tsc run over the four specs found the same pre-existing errors as on their main copies (navigation 7, huddle 17, scroll-history 0, custom-emoji 2).

Pushed with git push --no-verify, because the pre-push suites fail when the laptop is under load.

Readiness

An agent review of the combined flakey-tests branch (b432453af) found no blockers. buzz-review-completed is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

Siblings

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: d342cf141cb3211c8e5a6c74f881cf1d39ad3b12...832737efaed9b9b6d066777fc0d66287b72d7426
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

The authorized range changes only four desktop end-to-end tests. Inspection of the changed assertions and related focus, acknowledgement, tooltip, and scroll behavior found no concrete security, correctness, or reliability issues.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only source and history inspection. Tests and builds were not executed, as requested.

Generated by Codex Security Review |
Requested by: @matt2e |
Workflow run

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 :bot: Jude’s code review agent — REQUEST CHANGES

Reviewed: 1eb67bb525d66448b56d79f98c4ba0e2188b44c5..13171fdd5c763b3538b0c303c48461fa4c1ccd9d (exact head 13171fdd5c763b3538b0c303c48461fa4c1ccd9d)
Risk: Low runtime risk / medium validation risk. The diff is test-only, but the PR’s purpose is to make specific Desktop Playwright flakes deterministic, so a same-head failure in one of those changed specs means the proposed synchronization still does not prove its contract.

Finding

  1. [P2] Navigation tooltip flake fix still fails on the changed exact-head flake proof — desktop/tests/e2e/navigation.spec.ts:704

    The PR changes composer Buzz chip labels wrap without orphaning their icons to wait for the sent row ack, wait for the timeline floor to hold for two frames, and require the tooltip to have data-state="open" before asserting the tooltip center is over the current wrapped chip fragment. That is still not sufficient: the exact-head flake check failed this same test on the PR merge commit.

    Evidence from Flake Check / Flake check (Playwright smoke, 1/1) for run 41051895661, job 124049928438:

    Uses: block/buzz/.github/workflows/_ci-flake.yml@refs/pull/8743/merge (b114cdb1d91f85ebf4f540ded08dbbfbbc1973ce)
    ✘ [smoke] › tests/e2e/navigation.spec.ts:472:1 › composer Buzz chip labels wrap without orphaning their icons
    Expected: < 1
    Received: 81.3515625
    > 704 |       .toBeLessThan(1);
    at positionOverFragment (/home/runner/work/buzz/buzz/desktop/tests/e2e/navigation.spec.ts:704:8)
    

    This is the exact tooltip-positioning assertion this PR is trying to stabilize. A tooltip can be visible/open while still centered over the previous fragment, so the new data-state="open" assertion does not guarantee useInlineTooltipPosition has processed the pointer-driven fragment reposition. The result is that the advertised flake is still reproducible under the PR’s own repeated smoke run.

    Author action: tighten positionOverFragment so each fragment hover deterministically causes the tooltip to reposition over that fragment before the center assertion runs. For example, close/reopen or otherwise force a fresh hover lifecycle per fragment, or wait on an observable that proves the pointer-driven reposition for the target fragment has actually landed. Then rerun the exact-head Playwright flake check and get this changed spec green.

    Verification owner: author + CI flake check.

Validation

  • PASS — git rev-parse HEAD in the review checkout: 13171fdd5c763b3538b0c303c48461fa4c1ccd9d.
  • PASS — git status --short | wc -l: 0 dirty entries.
  • PASS — gh api user --jq '.login': jedwards27; live PR author from gh pr view: matt2e.
  • PASS — gh pr view https://github.com/block/buzz/pull/8743 --json headRefOid,baseRefOid,statusCheckRollup: live head still 13171fdd5c763b3538b0c303c48461fa4c1ccd9d; base 1eb67bb525d66448b56d79f98c4ba0e2188b44c5; Flake Check / Flake check (Playwright smoke, 1/1) is FAILURE.
  • FAIL — gh run view 41051895661 --job 124049928438 --log-failed: changed navigation.spec.ts:472 failed at navigation.spec.ts:704 with tooltip center delta 81.3515625.
  • NOT RUN — local Desktop E2E/native/manual. The exact-head repeated CI smoke failure already establishes the author-actionable blocker for this test-only flake-fix PR.

Residual risk: I did not perform native/manual UI validation because this change is test-only and the exact changed automated proof is red. Re-review should start from the next head after the navigation flake proof is fixed.

matt2e added a commit that referenced this pull request Oct 30, 2026
…rt (#8741)

## Summary

Two RunnerTests fixes from the 23–30 Oct flake audit, both in
`mobile/ios/RunnerTests/RunnerTests.swift`. Both are test-only. This is
one of six PRs split from the reviewed `flakey-tests` branch (siblings
below).

### `testLargeTitleTextFadesWithScrollAndCompactTitleCanReverse` (3
runs)

- **Runs:** 40748163526 (job 123028155856), 40826294064 (#8679, job
123290415194, passed on attempt 2) and 40921441156 (#8694, job
123617149320), all in Mobile (iOS), attempt 1. None of the branches
touches the navigation bar. Signature:
`XCTAssertLessThan(visibleOpacity(of: centeredTitle), 0.99)` read
0.9999499, 1.0 and 1.0.
- **Cause:** collapsing adds a 0.18 s `CABasicAnimation` on the compact
title mask's opacity (`IosNavigationBar.swift` `applyScroll`). The test
slept 60 ms and expected the presentation opacity to be mid-fade. The
presentation layer follows wall-clock media time. When the Flutter app
in the test host keeps the main thread busy, `Task.sleep` resumes after
the fade has finished.
- **Change:** drop the 60 ms sleep and the mid-fade samples. Right after
`scroll(to: boundary)`, which runs the handler synchronously, check that
the mask's model opacity is 1. Also check that it carries a
`"titleFade"` opacity `CABasicAnimation` from 0 to 1, lasting 0.1–0.5 s.
Nothing turns the run loop before the check, so the animation is still
attached however long the main thread stalls. After the scroll back, a
new `XCTAssertNil` checks that reversing removes the fade. The later 240
ms waits and their end-state checks are unchanged.
- **Mutations that should fail it:**
- Delete `compactTitleMask.add(fade, forKey: "titleFade")`: the
`XCTUnwrap` throws.
- Delete `removeAllAnimations()`: the new `XCTAssertNil` fails. The old
test missed this.
  - Set `fade.duration = 0.001`: the band's lower bound fails.

### `testNativeRemovalConfirmationUsesDestructiveAlertAndCancelsOnce` (1
run)

- **Run:** 40950548609 (#8433, Dart only, job 123717768224), attempt 1.
Signature: `fulfillment(of: [completed], timeout: 3)` expired.
- **Cause:** the busy test-host main thread that #8678 handled for other
classes. The cancel reply comes from UIKit's dismissal completion, after
the dismiss animation runs on that thread.
- **Change:** raise the wait to 10 s, matching #8678's
`callbackTimeout`. The assertion is unchanged. Dropping `resolve` from
the dismissal completion still fails it, and so does resolving before
`alert.dismiss`.

### Related issue

None found. **Conflicts with #8568** ("Adapt iPhone Duo layouts, rail
navigation, and native sheets"). #8568 rewrites the fade test into a
loop but keeps the 60 ms sleep and the mid-fade samples. It already
conflicts with `main`, and this PR adds `RunnerTests.swift` to its
conflicts. #8665 only adds a voice-note test.

### Testing

Not compiled against the app or run locally, because Xcode and
RunnerTests bog this machine down. `swiftc -parse` accepts the file. The
new assertions, copied into a stub `XCTestCase`, typecheck with `swiftc
-typecheck` against the iOS simulator SDK and XCTest (Swift 5 mode). The
full file needs Flutter and the Buzz module, so it wasn't typechecked as
a whole. **The Mobile (iOS) lane is the check.**

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

### Siblings

- #8739 ci: Hermit fetch deadline and sccache fallback
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus
- #8744 test(desktop): five settle classes and remote-owned-mentions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
matt2e added a commit that referenced this pull request Oct 31, 2026
…kspace (#8744)

## Summary

Units 6–9 and 14 of the 23–30 Oct desktop Playwright flake audit, in two
commits. The specs now settle before they measure or act, and
remote-owned-mentions clears its draft reliably. Everything is
test-only. This is one of six PRs split from the reviewed `flakey-tests`
branch (siblings below).

### Unit 6: measuring before an animation finishes

Each spec now calls `waitForAnimations(page)` before measuring.

- `onboarding.spec.ts:1487` (#8715, Integration shard 1; the centring
check at :1545 receives 5.13): `showPage("owned")` slides content in
from `translateX(48px)` over 650 ms. The spec reads its three boxes in
separate `boundingBox` round trips, so they can come from different
points of the slide.
- `onboarding.spec.ts:2289` (main 40746677244 and 40925172215, and
#8666; the spacing check at :2485 receives 4.255): the avatar dialog has
a 200 ms zoom-in and a 250 ms height transition.
- `projects-v3-screenshots.spec.ts:280`, at :893 (main 39643531998,
40759209995 and 40969909906, plus four runs with fractional menu-edge
values): the context rail narrows from 380 to 280 px over 200 ms.

### Unit 7: `fill()` racing a caret move the app makes a frame later

- `mention-recipients.spec.ts:376` and `:97` (main 40969965981 and
40969858676): edit mode focuses the end of the composer in a
`requestAnimationFrame`. If that frame lands mid-`fill()`, the edit is
appended instead of replacing the text. **Change:** use #8682's
`replaceComposerText` at both call sites.
- `workflow-local-controls.spec.ts:151` (main 40926393897 and
40957616146, and #8239): `selectVariable` restores the caret in a
`requestAnimationFrame` after the spec's poll has passed. **Change:**
wait two animation frames before filling.

### Unit 8: clicks that hit the wrong element

- `profile.spec.ts:1128`, at :1543 (#8523): stacked Sonner success
toasts intercept the pointer. **Change:** a local `pressActionRow`
helper presses the five action rows with `toBeEnabled`, focus and Enter.
- `config-bridge-screenshots.spec.ts:314` (main 40967147375, and #8235):
`.last()` resolved while avatar fallbacks were still filling in on 200
ms timers, so it opened nadia's panel instead of Charlie's. **Change:**
pick Charlie's row by its text, and check the panel's name before
opening Runtime.

### Unit 9: the `createWorkflow` copies (preventive)

- `navigation.spec.ts:153` (7 failures on main, none since #8681; 14
first-attempt failures in Smoke shard 7): an unscoped
`getByLabel("Message text")` can fill the trigger's filter while
`AnimatePresence` keeps it mounted. **Change:** wait for the step
inspector, then fill inside it, as `workflows.spec` already does. The
wait now lives in one `addSendMessageStep` helper in
`tests/helpers/workflows.ts`, which `navigation.spec.ts` and
`workflows.spec.ts` (`createWorkflow`, :716, :820) all call.
- `workflow-reaction-picker.spec.ts:203-204` is left out. Its Reaction
Added trigger has no control labelled "Message text".

### Unit 14 (second commit): `remote-owned-mentions.spec.ts:566`

- **Run:** main 40998438103 (Smoke shard 9, job 123877347301). It passed
on retry and failed 1 of the 6 attempts it got in the window.
`sourceRecord()` at :673 returned thread A's draft instead of null.
- **Cause:** on ProseMirror, `fill("")` selects through the DOM, then
presses Delete. If ProseMirror hasn't seen the selection yet, it blocks
the Delete. Navigating to B then saves A's live text as its draft. This
is not a product bug: had the editor really been empty, the cleanup
would have cleared A's draft.
- **Change:** delete the text with one Backspace per character from the
end, then wait for the composer to read empty. The guard assertions are
unchanged.

### Related issue

None found. Sibling #8743 also edits `navigation.spec.ts`, in a separate
hunk. The two merge cleanly in either order. No open PR touches the
tests changed here. #6134, #8187, #5778, #8622, #7452, #7812, #8244 and
#3342 edit other tests in the same files.

### Testing

Not run locally, because Playwright bogs this machine down. Flake Check
will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass,
and biome's 3 warnings are in untouched files. Ad hoc `tsc` runs over
the touched specs and the helper found the same pre-existing errors as
on their `main` copies.

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

### Siblings

- #8739 ci: Hermit fetch deadline and sccache fallback
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8741 test(mobile): RunnerTests title fade and alert wait
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
matt2e added a commit that referenced this pull request Oct 31, 2026
)

## Summary

Two CI-workflow fixes from the 23–30 Oct flake audit. This is one of six
PRs split from the reviewed `flakey-tests` branch (siblings below). Each
PR applies on its own to `main`.

### Hermit tool downloads (5 runs, 1 on main)

- **Runs:** 40635610550 and 40941660945, in Detect Changed Paths
(`node`). The selection step printed nothing for 9–10 min, the job hit
its cap, and every gate failed as cancelled. Three more, 40373404267 on
main (PostgreSQL Tests and Desktop E2E Integration 1/3, `pgschema`),
40373003333 (Mobile Android, `cmake`) and 40373467575 (Desktop Build
macOS, `just`), got 504s during GitHub's release-asset outage on 10-26,
~17:54–18:00 UTC.
- **Cause:** jobs start with an empty Hermit cache, so a tool's first
call downloads it, with no deadline. Hermit v0.54.1 tries three times, a
second apart, then gives up.
- **Change:** a new `scripts/ci-hermit-retry.sh` runs a no-op tool
command with a 90 s deadline per attempt, three attempts and 15 s × n
backoff. The deadline is plain bash: job control gives each attempt its
own process group, which gets TERM and then KILL. macOS runners have no
`timeout(1)` and may run bash 3.2. A "Fetch Hermit tools" step calls it
for:
- `node` and `just` in Detect Changed Paths, where one stall cancels
every gate. One `sh -c` invocation covers both, so the worst case is 345
s, inside the 10-minute cap. Two invocations would take 690 s.
- `pgschema` in PostgreSQL Tests, Desktop E2E Integration and Backend
Integration (via `--help`, since it has no `--version`).
  - `just` and `cmake` in the Android lane.
  - `just` in Desktop Build (macOS).

The test, `scripts/ci-hermit-retry.test.mjs`, joins Detect Changed
Paths' `node --test` list, and the script joins the rust and mobile path
filters. One stalled attempt costs ~105 s, which fits every job's cap.
- A killed attempt doesn't break the retry. Hermit downloads to a temp
file and unpacks to a temp dir, and renames each into place only once
it's complete. Its lock is a kernel flock.

### sccache storage failures (2 runs)

- **Runs:** 40355289475 (Desktop E2E Relay postgres-tests, 10-26,
"Egress is over the account limit") and 40714034945 (relay-binaries,
10-28, "send http request").
- **Cause:** the sccache server checks its GHA cache storage at startup
and won't start if that fails. Cargo's first `sccache rustc -vV` spawns
it, so the build fails. `SCCACHE_IGNORE_SERVER_IO_ERROR` doesn't help
here: in v0.16.0 it is read only when a running server's compile
response fails.
- **Change:** a "Start sccache" step runs `sccache --start-server`. It
outputs `wrapper=sccache`, or an empty wrapper and a warning if the
server won't start. The three build steps take `RUSTC_WRAPPER` from it,
and cargo treats an empty `RUSTC_WRAPPER` as no wrapper.
- The fallback doesn't fail the job later, in sccache-action's post
step. At the pinned fc920bf, that step runs `sccache --show-stats` and
the JSON form, and fails the job if either exits non-zero. With no
server running, v0.16.0's `--show-stats` doesn't start one. It returns
zero stats from the client side and exits 0. So the step keeps its stats
annotation, and `disable_annotations` stays unset.

### Related issue

None found. **Overlaps #8213** ("ci: fix empty root-workspace Rust
caches and merge test-unit nextest runs"). #8213 already conflicts with
`main`, and this PR adds a conflict in `.github/workflows/ci.yml` (path
filters).

### Testing

Verified locally:
- With a fresh Hermit state dir, `node` was SIGKILLed mid-download, then
mid-unpack while holding the lock, then stopped with TERM by the
script's own 1 s deadline. Each retry produced a working v24.15.0, at
clean-unpack speed. The `sh -c` form fetched both tools, and a 1 s
deadline on `sh -c 'sleep 37 && ...'` stopped `sh` and its child,
leaving no orphan. Tested under macOS bash 3.2 and Debian bash 5.2.
- sccache v0.16.0 (aarch64-apple-darwin) ran against a GHA cache whose
`ACTIONS_RESULTS_URL` refused connections, in READ_ONLY and READ_WRITE
modes. `--start-server` exited 2 ("Server startup failed: cache storage
failed to read"). Both `--show-stats` forms then exited 0, with zero
stats and valid JSON. `--stop-server` exited 2, which shows no server
had started.

Only hosted CI can show:
- the script under the runners' real shells, and that healthy runs only
pay a few seconds;
- whether ~3 min of spaced attempts rides out the next outage. The 10-26
outage lasted ~6 min, longer than this covers;
- that the explicit `--start-server` behaves like cargo's implicit start
against the real GHA cache. The fallback branch needs a storage outage
to run in CI.

Not covered here: first fetches of other tools (rustup, uv, pnpm) still
have no deadline. A per-job Hermit package cache is the follow-up.

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

### Siblings

- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8741 test(mobile): RunnerTests title fade and alert wait
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus
- #8744 test(desktop): five settle classes and remote-owned-mentions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Units 2-5 of the 23-30 Oct desktop Playwright flake audit. All four are
test-only. origin/main (1eb67bb, now including #8680) hasn't touched
these specs since fae3f03. Of the open PRs, #8622 adds an unrelated
test at the end of navigation.spec, and #7812 (stale since 10-06) adds
tests elsewhere in three of them. #8680 didn't touch them, but it
carried navigation:472 mode A's product half (eb88a98), which is not
part of this commit. The code confirms each cause, so none is dropped.

navigation.spec.ts:472, "composer Buzz chip labels wrap without
orphaning their icons"

Runs: 11 jobs in the window, 6 after the earlier fixes, and 4 of the 7
main runs that carry every fix. Run 40970053997 (Smoke shard 7) failed
all three attempts.

Mode A: jobs 122971493553, 123066839967, 123572467962, 123819180597
and retry 2 of 123784825042. Signature: fragmentStep is null at :634.

Cause: rows key on renderKey = localKey ?? id. On main,
reconcileChannelWindowMessages keeps only pending sends and thread
replies from the cache, so a head refetch or live echo after the ack
drops the localKey. The key flips and the row remounts, losing the
220 px width the test set in an earlier evaluate.

Change: re-land the test half of closed PR #6852, which never merged.
Set the width and measure in one expect.poll, and set the width again
before each fragment measurement.

Mode B: jobs 123573056202, 123784199542, 123784825042 (twice),
123804428893, 123818728963, 123819543461, and 122924164125 (the
Infinity variant). Signature: the |tooltipCenter - cursorX| < 1 poll
at :667 receives 81.35 or Infinity. In failing attempt 40970053997 the
post-send settle (scrollTop 200, 296, 260, 302) took its last step
21 ms or less before focus, and the tooltip read closed 12 ms after
it. In the passing retry 40980513346, scrolling had stopped at least
15 ms before focus.

Cause: Radix Tooltip 1.2.16 closes when an ancestor of its trigger
scrolls (a capture-phase window listener). Reopening by pointer needs
a pointermove while hasPointerMoveOpenedRef is false, then the chip
provider's 500 ms delay. Any close clears that timer, and only
pointerleave resets the flag. useInlineTooltipPosition moves the
content only on pointermove and placement. So a scroll after focus can
leave the tooltip closed, or open without the fragment offset, and the
spec's single mouse.move never recovers it.

Change: before focusing the chip, poll until the timeline sits at its
floor (scrollHeight - clientHeight - scrollTop <= 1) at the start of
one poll, is still there two animation frames later, and its scrollTop
hasn't moved. Waiting only for scrollTop to hold for two frames isn't
enough: a timeline whose settle hasn't started yet reads as still, and
the failing run's last step landed under two frames before focus. The
settle's end state is the floor. useVirtualizedBottomSettle scrolls
only when Virtua's ResizeObserver reports a layout change, and after
the send and the 220 px wrap the timeline sits short of the floor
until the settle catches up. Focusing the chip and moving the pointer
over it don't change layout. The send's ack does. It removes the row's
"Sending…" status, and the confirmed row can lose its header, since a
pending row is never shown as a continuation (MessageRow.tsx:167). The
send's bottom intent is still armed, so that re-pins the timeline. So
before the floor poll, wait for the sent row's message-send-status to
reach count 0. If the ack has already landed, that wait is a no-op. The
standalone toBeVisible after focus is gone. positionOverFragment
asserts data-state matches /open/ instead, because a closing tooltip
stays visible through its exit animation.

No retry. Wrapping the measure, move and check in toPass, leaving and
re-entering the chip on each attempt, would also pass, but it retries
around the race instead of removing it. The ack and floor waits close
the scroll window. The only other way to scroll under the tooltip was a
remount that drops the row's 220 px width (mode A), and #8680's
retainAcknowledgedLocalKeys, on this branch's base (1eb67bb), keeps
the row's key. So positionOverFragment is a single pass, as on main,
plus mode A's width re-set: set the width, measure, move, assert open,
then poll the centre with the default timeouts.

huddle-transcription.spec.ts:637, "animates the responding agent with
the shared speaker ring"

Runs: 9 jobs, always Smoke shard 4 attempt 1. Main: 39896948198,
40718743531, 40732061485, 40840949170, 40906253761, 40969965981 and
40983029157. PRs: #8715, #8666. Signature: toBeFocused() at :697
reads inactive for all 14 polls over 5 s. At +3.0 s in the failure
video (40983029157, job 123827818084) the caret is in
"Message #huddle".

Cause: useComposerAutofocus re-runs when `focus` changes. That's
focusEnd, which depends on the editor, so it runs again once the
editor mounts. It backs off only for input, textarea, select and
contentEditable focus, so it takes focus from the focused trigger.

Change: wait for message-input to be focused before focusing the
trigger. The same spec already does this at :855.

scroll-history.spec.ts:1289, "fast middle-page scroll settles with
continuous mounted coverage"

Runs: 9 jobs in the window. After the fixes: main 40980404335 (Smoke
shard 9, job 123819180518), two attempts on #8720, and the
brain/native-account-setup branch. Signature: the viewportCoverage
poll expects < 100 and receives 200. The failure screenshot shows the
timeline at the floor. The 200 px is the bottom spacer
(--composer-overlay-height) under the last row.

Cause: TimelineMessageList arms useVirtualizedBottomSettle when the
channel first positions. Its ResizeObserver then pins to the floor
until listenForReaderScroll input or an accepted target cancels it.
handleScroll deliberately doesn't cancel, and a prepend delta doesn't
settle, so the intent survives the prepend and the spec's scrollTop
writes. Virtua's re-measure after the jump pulls the list back down.

Change: before the fast scroll, hover the timeline and wheel up 120 px.
Then poll until it is more than 32 px from the floor (the timeline's
at-bottom band) and scrollTop holds for two frames, so the writes stay
the last scroll.

custom-emoji.spec.ts:370, "reacting with a custom emoji renders via
the loopback media proxy"

Runs: main 40787609859 (10-28), main 40983029157 (Smoke shard 3, a
run with every fix), and #8241 (agent and auth files only). Signature:
the poll at :453 expects the inline "Add reaction" opacity "0" and
receives "1". The error context marks button "Open reactions"
[active].

Cause: the non-modal Radix Popover returns focus to its trigger when
the picker closes. FocusScope does that in a setTimeout(0) after the
content unmounts, and the content unmounts after its 100 ms exit
animation. That can land after the spec's blur. The action bar is
hidden by opacity only, so the trigger takes focus, and
group-focus-within/message keeps the button visible.

Change: wait for the "Open reactions" trigger to be focused, then
blur. The note suggested waiting for em-emoji-picker to reach count 0.
That still leaves the setTimeout window open. Focus reaches the
trigger only after unmount, so this wait covers both.

Verification

Not run locally: the Flake Check lane will repeat the touched specs.
pnpm typecheck and pnpm check pass. biome's 3 warnings are in
untouched files. An ad hoc tsc ran over the four specs, using the
desktop tsconfig with only those files, and over HEAD's copies of
them. Both gave the same error counts: navigation 7, huddle 17,
scroll-history 0, custom-emoji 2. The messages are identical once
line numbers are stripped, and all are pre-existing: no Node types,
findLast against the ES2022 lib, and the leadingBounds narrowing at
navigation:576. With the floor wait in place and the retry dropped,
navigation still gives the same 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e force-pushed the flakey-tests-desktop-e2e-scroll-focus branch from 13171fd to 832737e Compare November 2, 2026 03:19
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@matt2e
matt2e deployed to codex-review November 2, 2026 03:19 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026

This branch was successfully deployed

1 active deployment
codex-review — 832737ef Deployed Nov 2, 2026 by matt2e via Run Codex Security Review #11684
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants