Repository navigation
test(browser): wait on the signal each test asserts in local-agent-activity and message-navigation - #3444
Conversation
…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>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
🤖 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 }); |
There was a problem hiding this comment.
🤖 [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.
…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
* 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
* 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
Why
Two journeys fail intermittently in CI because each waits on a signal that does not match what the test then asserts.
local-agent-activity.spec.mjs, "opens an active message's empty thread without losing its activity preview"local-agent-activity.spec.mjs, "finishing activity retires keyboard controls without stealing focus: stop"message-navigation.spec.mjs, "exact reveal uses ordinary dwell rather than marking read at open"Root cause and fix
One commit per spec.
Thread rows still positioning. While a thread positions its first page,
ThreadPanelrenders its rows withinertand setsaria-busyon the "Thread messages" region. Playwright'stoBeVisible()ignoresinert, andlocator.press()andfocus()make no actionability check. The two steps added in #3140 could therefore run before positioning finished.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'stoBeFocused()passes early because the composer sits outside the inert rows.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"andinerton 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 signalchannel-lifecycle,nested-repliesandnavigation-thread-historyalready use. The wait sits after the composertoBeFocused()and before the Enter press, and in the finishing loop after the bubble is visible and before anyfocus(), 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
readJournalfromreading.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 byunread.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.local-agent-activity.spec.mjs -g "opens an active message|without stealing focus", WebKit,--workers=1 --repeat-each=8(4 tests × 8)message-navigation.spec.mjs -g "exact reveal uses ordinary dwell", Chromium,--workers=1 --repeat-each=8--repeat-each=4message-navigation.spec.mjs,--workers=2During 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
buzz-review-completedis not set.Left alone
notifications.spec.mjs.local-agent-activity.spec.mjs. Open PRs perf(threads): virtualize the thread panel's replies #2911, fix(messages): don't let a message reveal steal focus the person moved #2563, fix(channels): open the unread reply from sidebar Activity #2555, feat(unread): restore the Inbox's evidence from a device record on launch #2407 and fix(unread): keep the viewer's threads in the Inbox under busier channels #2401 edit other parts ofmessage-navigation.spec.mjs. A trial merge of each against this change is clean.🤖 Generated with Claude Code