Skip to content

test(browser): wait on the signal each test asserts in local-agent-activity and message-navigation - #3444

Merged
matt2e merged 3 commits into
mainfrom
mtoohey/flake-readiness-signals
Nov 14, 2026
Merged

matt2e merged 3 commits into
mainfrom
mtoohey/flake-readiness-signals

Conversation

@matt2e

@matt2e matt2e commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Why

Two journeys fail intermittently in CI because each waits on a signal that does not match what the test then asserts.

Test Sightings
local-agent-activity.spec.mjs, "opens an active message's empty thread without losing its activity preview" Browser journeys: 10 Nov, main, WebKit 18/18, 10 Nov, Chromium 9/9, 11 Nov, WebKit 18/18, 11 Nov, WebKit 18/18
local-agent-activity.spec.mjs, "finishing activity retires keyboard controls without stealing focus: stop" Browser journeys: 10 Nov, WebKit 18/18
message-navigation.spec.mjs, "exact reveal uses ordinary dwell rather than marking read at open" Browser journeys: 10 Nov, main, Chromium 5/9

Root cause and fix

One commit per spec.

Thread rows still positioning. While a thread positions its first page, ThreadPanel renders its rows with inert and sets aria-busy on the "Thread messages" region. Playwright's toBeVisible() ignores inert, and locator.press() and focus() make no actionability check. The two steps added in #3140 could therefore run before positioning finished.

  • In the empty-thread test, activity.press("Enter") went to the still-focused reply composer, which is prefilled with a mention. WebKit sent the draft and raised the "Mention people outside this channel?" dialog. Chromium kept focus in the composer. The Activity tab never appeared. The composer's toBeFocused() passes early because the composer sits outside the inert rows.
  • In the "stop" variant, focus() on Stop was a no-op, so the handoff to "More message actions" never happened.

DOM snapshots from the four traces examined show aria-busy="true" and inert on the rows at the moment of the press or focus. Every sighting post-dates #3140. This is not a product bug: people cannot reach inert rows either.

The tests now wait for the region's aria-busy="false", the readiness signal channel-lifecycle, nested-replies and navigation-thread-history already use. The wait sits after the composer toBeFocused() and before the Enter press, and in the finishing loop after the bubble is visible and before any focus(), so the bubble variant is covered as well.

Read publication under a default timeout. The exact-reveal dwell test ended by polling the fixture's read publications with the default 10 s timeout. A publication needs the 300 ms reading dwell, then read-state's 5 s publish debounce, then the flush's read, sign and publish. That leaves about 4.5 s of slack. In the failing trace the flush's read reached the fixture on time and the relay answered in about 4 ms, but the browser took 5.6 s to receive the response body. The signing request arrived 0.9 s after the poll deadline. The evidence file at teardown held the publication, so the behaviour was correct. The test was waiting on the wrong signal for what it asserts, which is that dwell marks read and open does not.

The test now asserts the local read with readJournal from reading.mjs, as the sibling test in the same file does. Right after the held query is seen, the journal's frontiers are still empty. At the end it polls for a non-empty frontier set. The journal write is strictly earlier than the publication and does not depend on the relay round-trip. The existing check that publications are unchanged at open is kept. The publication path stays covered by unread.spec.mjs, which polls with a 12 s timeout.

No timeouts or tolerances changed. No browser cases were added or removed.

Evidence

Local runs on Linux, bin/pnpm test:browser:ci --no-deps --reporter=list.

Scope Result
local-agent-activity.spec.mjs -g "opens an active message|without stealing focus", WebKit, --workers=1 --repeat-each=8 (4 tests × 8) 32/32 passed
Same, Chromium 32/32 passed
message-navigation.spec.mjs -g "exact reveal uses ordinary dwell", Chromium, --workers=1 --repeat-each=8 8/8 passed
Same, WebKit, --repeat-each=4 4/4 passed
Whole message-navigation.spec.mjs, --workers=2 Chromium 22/22, WebKit 22/22

During triage, a test-only route that delayed the relay query by 1.5 to 2.5 s reproduced the inert-row failure deterministically: without the wait WebKit and Chromium each failed 2/2 with the CI signature, and with the wait all passed.

Not reproduced, not run

  • Neither failure reproduced on an idle host without that injected delay. The slow response body in the read-publication failure was pinned to CI load. The repeat runs guard against regression rather than reproducing CI timing.
  • No human has tested this yet, so buzz-review-completed is not set.

Left alone

🤖 Generated with Claude Code

matt2e and others added 2 commits November 13, 2026 13:39
…ing or focusing them

While a thread positions its first page, ThreadPanel renders its rows with
inert={positioning} and sets aria-busy={positioning} on the "Thread
messages" region. Playwright's toBeVisible() ignores inert, and
locator.press()/focus() make no actionability check, so the two steps
#3140 added could run before positioning finished. In the empty-thread
test, activity.press("Enter") went to the still-focused reply composer,
which is prefilled with "@Local tester": WebKit sent the draft and raised
the "Mention people outside this channel?" dialog, Chromium just kept
focus in the composer, and the Activity tab never appeared. The
toBeFocused() on the composer passes early because the composer sits
outside the inert rows. The "finishing activity … stop" variant failed the
same way: focus() on Stop was a no-op, so the handoff to "More message
actions" never happened. DOM snapshots from all four failing traces show
aria-busy="true" and inert="" on the rows at the moment of the press or
focus. Not a product bug: users cannot reach inert rows either.

Wait for the region's aria-busy="false" — the readiness signal
channel-lifecycle, nested-replies and navigation-thread-history already
use — after the composer toBeFocused() and before activity.press("Enter"),
and in the finishing loop after the bubble is visible and before any
focus(), so the bubble variant is covered as well.

Verified with `bin/pnpm test:browser:ci --no-deps --workers=1
--repeat-each=8 --reporter=list tests/browser/local-agent-activity.spec.mjs
-g "opens an active message|without stealing focus"` (4 tests × 8):
- --project webkit: 32/32 passed (2.5 m; 4.1–4.2 s and 3.3–3.4 s).
- --project chromium: 32/32 passed (2.1 m; 3.1 s and 2.5–2.7 s).
The triage only reproduced the inert-row press by delaying the relay
query, not on an idle host, so these runs guard against regression rather
than reproduce CI timing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…er an exact reveal

The exact-reveal dwell test ended by polling app.report.readPublications
with the default 10 s expect timeout. A publication needs the 300 ms
reading dwell (use-reading.ts), then read-state's 5,000 ms publish
debounce (read-state.ts schedule()), then flush()'s own-slot read, sign
and publish, leaving about 4.5 s of slack. In the failing CI trace the
flush's read reached the fixture on time and the relay answered in ~4 ms,
but the browser took 5.6 s to receive the response body, and
read-state-sign arrived 0.9 s after the poll deadline. evidence.json at
teardown held the publication, so the behaviour was correct; the test was
waiting on the wrong signal for what it asserts (dwell marks read, open
does not).

Assert the local read instead, with readJournal from reading.mjs as the
sibling test in this file already does: right after the held query is
seen, the journal's frontiers are still {} (nothing is marked read at
open), and at the end poll for a non-empty frontier set. save() awaits
storage.update() before schedule() arms the debounce, so the journal
write is strictly earlier than the publication and does not depend on
the relay round-trip. The publication path itself stays covered by
unread.spec.mjs, which polls readPublications with timeout: 12000. The
existing readPublications-unchanged check at open is kept.

Verified with `bin/pnpm test:browser:ci --no-deps --reporter=list
tests/browser/message-navigation.spec.mjs`:
- --project chromium --workers=1 --repeat-each=8 -g "exact reveal uses
  ordinary dwell": 8/8 passed (3.4–3.5 s each, 1.1 m).
- --project webkit --workers=1 --repeat-each=4 -g "exact reveal uses
  ordinary dwell": 4/4 passed (4.3 s each, 36.8 s).
- whole file, --workers=2: Chromium 22/22 (1.5 m), WebKit 22/22 (2.4 m).
Biome is clean on the file. The slow body read did not reproduce on an
idle host; the triage pinned it to CI load, so the local runs guard
against regression rather than reproduce CI timing.

Not addressed here: the same default-timeout readPublications poll after
page.clock.resume() in "settings blocks reading and focus in a retained
thread" (this file) and in notifications.spec.mjs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e requested a review from a team as a code owner November 13, 2026 02:49

@kalvinnchau kalvinnchau 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.

🤖 One timing-dependent test assertion needs a controlled clock.

…ot race dwell

Review of PR 3444 found that the empty-journal assertion added in
eb5165c races the behaviour it sits next to. ThreadPanel's
completeTarget sets readingSettled and dispatches reading-positioned
before it completes navigation, so use-reading.ts starts the target's
300 ms dwell before openTarget() even resolves. The held depth query
does not stop it: the exact target is already shown and focused. If the
test needs more than 300 ms of real time to get from there to its
IndexedDB read, correct behaviour has already written the target's
frontier and `toEqual({})` fails. The check only passed because an idle
host gets there in time.

Confirmed before changing anything: with a temporary 400 ms wait after
`await seen`, the eb5165c test failed 2/2 on Chromium and 2/2 on
WebKit at that assertion, each time with exactly one frontier,
`msg:<target id>: 1700000182`. Unmodified, it passed 2/2 on WebKit.

Control the page clock instead, the way "settings blocks reading and
focus in a retained thread" below already does:

- Install the clock before the page loads, and pause it at a fixed
  offset once startup, unread.ensure() and the route are in place.
- Start openTarget() without awaiting it and step 16 ms frames until
  navigation reports "opened". Exact reveal needs two animation frames,
  and Playwright's clock fakes requestAnimationFrame, so a paused clock
  would otherwise stall it. Dwell can have earned at most one frame.
- Assert the journal is empty and readPublications is unchanged (both
  kept), and that the target is focused. Fake time is frozen here, so
  no amount of real time lets dwell elapse.
- Advance 300 ms and poll the journal for exactly
  `{ "msg:<target id>": target.created_at }`. This replaces the old
  "some frontier exists" poll and is exact because the target is the
  only thread row while its context query is held. The poll only waits
  for the asynchronous IndexedDB write; it advances no time.
- Release the held query, resume the clock, and keep the 81-row and
  focus checks.

The held query, the route handler and navigation completion run on
network and promise callbacks, which the clock does not fake. The
sibling test's holdReadingFocus() is not needed: startup leaves focus
on <main>, outside every reading surface, and the journal was `{}` at
the pause in every run. No timeouts or sleeps were added, and nothing
outside this test changed.

Verified on a loaded local macOS host (load average 34 to 42) with
`bin/pnpm test:browser:ci --no-deps --workers=1 --reporter=list
tests/browser/message-navigation.spec.mjs -g "exact reveal uses
ordinary dwell"`:
- --project=chromium --repeat-each=8: 8/8 passed (1.2 m, 3.4 to 4.1 s).
- --project=webkit --repeat-each=8: 8/8 passed (1.5 m, 3.8 to 6.1 s).
- With the temporary 400 ms wait after `await seen` in the fixed test,
  --repeat-each=2: Chromium 2/2 and WebKit 2/2 passed. The wait was
  removed before committing.
- A temporary dump of the fake timers just before the 300 ms advance
  (5 Chromium runs, 3 WebKit) showed the same state every time: clock
  at 20,064 ms, one thread row, dwell timer due at 20,364 ms, and after
  the advance the 5 s publish debounce armed at 20,364 ms. The dwell
  was never restarted after "opened".
- Biome is clean on the file; `git diff --check` is clean.

Not run: the rest of message-navigation.spec.mjs, any other spec, and
Linux engines. Hosted CI covers those.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>

@kazemek21 kazemek21 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.

Kyle reviewed the PR and the Nexus review-tracker notes and approved with no comments.

Posted by Codex (gpt-6.1-sol) on behalf of kylez.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s account. Reviewed 0ab64f7d313453b608fda90ac7d59a807d6dae5b against 7f03764beb8eb0ddcb4ff9ec0b8bda3e07699716.

One P2: the exact-reveal clock repair controls target dwell, but startup can still earn a channel read before the clock is paused. Details and the existing helper to reuse are inline. Thread readiness waits match the state controlling inert rows, and the target-frontier assertion preserves the intended local durability boundary.

Validation: source review only; the startup race was not reproduced in this review, and no tests or app execution were performed. Existing hosted run 43379266580 records 10 passing repetitions of each of the five changed scenarios in both Chromium and WebKit (100/100). Artifacts bind to clean merge 641d207adf54abf0ced682bccbb2b961a221578a, containing this head and earlier target 7c786bb464a72fd7ea25084a9f57edd4b347093d; these passes do not establish the disputed startup ordering or validate the current-base combination. No browser cases were added or removed. Timing artifacts were inspected; no matched before/after baseline was established.

// only a paused clock keeps "nothing read at open" from racing that dwell.
// Pause at a fixed offset, not the page's already-ticking clock.
const base = Date.now();
await page.clock.install({ time: base });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 [P2] Hold startup reading focus before opening the app

page.clock.install() leaves time running until pauseAt below. During open(page, app), Messages initially autofocuses the channel composer, which qualifies for the normal 300 ms reading dwell. If a slow runner leaves that focus in place before the search palette takes over, Alpha can already save a frontier. Pausing afterward cannot undo it, so the empty-journal check and later target-only equality fail despite correct behavior. unread.spec.mjs:130–133 documents and guards this same startup ordering. Reuse holdReadingFocus(page) before open() and releaseReadingFocus(page) after pauseAt, as the neighboring retained-thread test does. This is a source-derived ordering defect; it was not reproduced in this review.

@matt2e
matt2e merged commit 6896e9e into main Nov 14, 2026
55 checks passed
@matt2e
matt2e deleted the mtoohey/flake-readiness-signals branch November 14, 2026 00:39
wpfleger96 added a commit that referenced this pull request Nov 14, 2026
…y-unreads

* origin/main: (24 commits)
  [1/2] feat(agents): add a recommended agent setup (core) (#3344)
  test(browser): survive the live-stream restart a page-clock jump causes (#3442)
  chore(renovate): hold rusqlite below 0.41 for mesh-llm (#3438)
  test(browser): serve the diffs and CLI reload fixtures outside the navigation budget (#3445)
  test(browser): play the draft clip at rate 0 instead of looping it before the Play click (#3446)
  fix(status): reopen a status whose preset deadlines coincide as Today (#3449)
  fix(messages): tolerate a cancelled reaction-name chunk load (#3450)
  fix(pairing): tolerate /proc/net/tcp rows printed twice or missed (#3451)
  test(browser): wait on the signal each test asserts in local-agent-activity and message-navigation (#3444)
  Expose native channel headings to Solo sources (#3523)
  test(browser-host): run the 100,000-channel broker scale check alone in the measurements lane (#3447)
  fix(solo): keep every contributed destination action (#3516)
  fix(identity): report an issuer that keeps rate-limiting an agent as a limit (#2365)
  fix(identity): let a stuck at-launch sign-in copy its link (#2453)
  fix(profiles): open avatar previews on hover in compact density (#3510)
  feat(solo): smooth chat recency reordering (#3502)
  Reject self-mentions that cannot wake another session (#3513)
  feat(status): offer don't clear for every status (#3515)
  Let plugin panels name their tab with panels.useTabTitle (#3251)
  fix(agents): report a conversation wake that fails before its turn starts (#2192)
  ...

# Conflicts:
#	tests/browser/message-management.spec.mjs
wpfleger96 added a commit that referenced this pull request Nov 14, 2026
* origin/main: (27 commits)
  [2/2] feat(agents): offer Automatic (Recommended) in Create agent and Identity (#3345)
  fix(solo): preserve and resume unsent chat drafts (#3525)
  fix(builderlab): give refused app sign-ins and Identity links a route to Organization sign-in (#3078)
  fix(agents): say why Import an agent can't import here, and link Manage plugins (#3073)
  [1/2] feat(agents): add a recommended agent setup (core) (#3344)
  test(browser): survive the live-stream restart a page-clock jump causes (#3442)
  chore(renovate): hold rusqlite below 0.41 for mesh-llm (#3438)
  test(browser): serve the diffs and CLI reload fixtures outside the navigation budget (#3445)
  test(browser): play the draft clip at rate 0 instead of looping it before the Play click (#3446)
  fix(status): reopen a status whose preset deadlines coincide as Today (#3449)
  fix(messages): tolerate a cancelled reaction-name chunk load (#3450)
  fix(pairing): tolerate /proc/net/tcp rows printed twice or missed (#3451)
  test(browser): wait on the signal each test asserts in local-agent-activity and message-navigation (#3444)
  Expose native channel headings to Solo sources (#3523)
  test(browser-host): run the 100,000-channel broker scale check alone in the measurements lane (#3447)
  fix(solo): keep every contributed destination action (#3516)
  fix(identity): report an issuer that keeps rate-limiting an agent as a limit (#2365)
  fix(identity): let a stuck at-launch sign-in copy its link (#2453)
  fix(profiles): open avatar previews on hover in compact density (#3510)
  feat(solo): smooth chat recency reordering (#3502)
  ...

# Conflicts:
#	src-tauri/src/federated_identity/agent/tests.rs
#	src-tauri/src/federated_identity/mod.rs
#	src-tauri/src/federated_identity/protocol.rs
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 14, 2026
* origin/main: (133 commits)
  Expose ordinary chats without workspace headings (block#3530)
  [2/2] feat(agents): offer Automatic (Recommended) in Create agent and Identity (block#3345)
  fix(solo): preserve and resume unsent chat drafts (block#3525)
  fix(builderlab): give refused app sign-ins and Identity links a route to Organization sign-in (block#3078)
  fix(agents): say why Import an agent can't import here, and link Manage plugins (block#3073)
  [1/2] feat(agents): add a recommended agent setup (core) (block#3344)
  test(browser): survive the live-stream restart a page-clock jump causes (block#3442)
  chore(renovate): hold rusqlite below 0.41 for mesh-llm (block#3438)
  test(browser): serve the diffs and CLI reload fixtures outside the navigation budget (block#3445)
  test(browser): play the draft clip at rate 0 instead of looping it before the Play click (block#3446)
  fix(status): reopen a status whose preset deadlines coincide as Today (block#3449)
  fix(messages): tolerate a cancelled reaction-name chunk load (block#3450)
  fix(pairing): tolerate /proc/net/tcp rows printed twice or missed (block#3451)
  test(browser): wait on the signal each test asserts in local-agent-activity and message-navigation (block#3444)
  Expose native channel headings to Solo sources (block#3523)
  test(browser-host): run the 100,000-channel broker scale check alone in the measurements lane (block#3447)
  fix(solo): keep every contributed destination action (block#3516)
  fix(identity): report an issuer that keeps rate-limiting an agent as a limit (block#2365)
  fix(identity): let a stuck at-launch sign-in copy its link (block#2453)
  fix(profiles): open avatar previews on hover in compact density (block#3510)
  ...

Signed-off-by: Joah Gerstenberg <joah@squareup.com>

# Conflicts:
#	src/bundled/codex/prompt.test.ts
#	src/bundled/goose/runtime.ts
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.

4 participants