Skip to content

test(console): wait for the settings page's registration release before the shell leaves after a save - #26212

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-26096-settings-shell-leave-wait
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-26096-settings-shell-leave-wait

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #26096

Clause-②: no
Metadata-protocol: none

What was wrong

Both queue ejections on this anchor (builds 40316529606 and 40337868008, at 1066 ms and 1064 ms) are the same case: CONTROLS … › after a successful save: the shell leaves at once, at findByText('Profile page'). It looked like a slow navigation that ran out of the 1000 ms default. It is not one. The click is swallowed, and the navigation never happens at all:

  1. The save's PUT resolves outside act, so React commits it as an ordinary Scheduler task. The unsaved bar goes in that commit.
  2. The page's unsaved-edits registration (useRegisterUnsavedEdits, through useUnsavedLeaveGuard) is released in that commit's passive effect. React runs that effect as a task of its own, after the commit.
  3. The test only waited for the bar. On a loaded worker the wait can return between (1) and (2): RTL's settle drain is a 0 ms timer, and when the event loop reaches its timers phase late it runs before the passive-effect task.
  4. The Profile click then calls confirmUnsavedLeave while the page is still registered. It asks window.confirm, the stub answers Cancel (false), and the route stays on /settings/localization. 'Profile page' never renders, so a longer timeout cannot help.

This is the same race the sibling harness SettingsView.leaveGuard-13163 fixed in its own save case (objectui#13317). That fix waits on the guard itself, through a cancelable beforeunload that is no longer prevented, and this PR applies it here.

The change

One file, test-only, +16 / -0: ui/apps/console/src/pages/settings/__tests__/SettingsView.shellLeave-13231.test.tsx. In the after-save case, after the existing bar waitFor, the test now waits until a cancelable beforeunload dispatched on window is no longer defaultPrevented. The seam installs that listener only while some page is registered, so the wait ends exactly when nothing is registered any more. The assertions are unchanged: Profile page appears, and confirm is never called. There is no timeout change, no sleep, no retry, no skip and no product change.

Evidence

All runs were under scripts/pm/os-verify-lock.sh on a shared box, pnpm exec vitest run --project @object-ui/console --maxWorkers=2.

  • Mechanism probe (throwaway file, deleted, not committed). A React Profiler onRender hook logged each commit. The save's commit runs with IS_REACT_ACT_ENVIRONMENT=false, and at that commit the bar is already gone while the registration is still held (held=true bar=false). The registration is released only later.
  • Deterministic reproduction on the unfixed case. The probe emulated a starved worker: once the bar goes, it burns 3 ms of CPU after the already-queued microtasks, so the event loop reaches its timers phase late. The unfixed case then failed 2 of 2 runs with registrationHeld=true at the click, confirmCalls=1, path still /settings/localization, and NOT FOUND after 1006 ms. That is the CI signature. Without the emulation the same probe passed.
  • The fix under the same emulation: 3 of 3 runs, 6/6 tests each.
  • Natural load on main: not reproduced. Plain runs of the file on main are green. I did not run a CPU hog on the shared container. The red above comes from the emulation only, and nothing else is claimed.
  • Fixed file, 5 runs in a row at f7b810d: 5 × Tests 6 passed (6), VERDICT command-exit 0.
  • Whole @object-ui/console project at f7b810d: Test Files 247 passed (247), Tests 2488 passed (2488), VERDICT command-exit 0.
  • After merging origin/main (head 9c7d1a0), deps rebuilt: this file plus SettingsView.leaveGuard-13163, Test Files 2 passed (2), Tests 18 passed (18), VERDICT command-exit 0.
  • Gates at 9c7d1a0: dispatch-gates --commands derived 23 commands, and all 23 exit 0. --ran reconciliation: 23 derived, 23 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --filter @object-ui/console run type-check exits 0, but that program does not list this test file (--listFiles count 0), so it does not cover the change.

Acceptance notes

  • The other two findByText('Profile page') sites (:162 and :184) are left as they are, on purpose. A probe showed 'Profile page' is already in the DOM synchronously after the click there (sync=true, found in 2–7 ms). Neither case has a commit outside act before the click. The OK case navigates whatever the registration state is, and the clean-page case never registers. Neither has ever ejected a PR.
  • No timeout was raised anywhere. The seat's suggested route was to size the findBy waits to a route-load budget. That would not have fixed this, because the navigation never happens. It would also break ui/AGENTS.md "测试纪律", which forbids fixing a flake by raising a timeout. The triage wording "await the save's settled state before querying" is what was done: the settled state that matters is the released registration.
  • Changeset: none. The diff is a test file, and @object-ui/console's files[] (dist, plugin.*, README.md) ships none of it, so the PR carries skip-changeset.

Generated by Claude Code

…re the shell leaves after a save

The after-save case of SettingsView.shellLeave-13231 waited only for the
unsaved bar to go. The bar goes in the save's commit; the page's
unsaved-edits registration is released a step later, in a passive effect
React runs as its own task because the save settles outside act. On a
loaded worker the bar-wait returned in between, the Profile click met a
still-registered page, the stubbed confirm answered Cancel, and the route
never changed, so findByText('Profile page') timed out at the 1000 ms
default (queue builds 40316529606 and 40337868008). A longer timeout
cannot help: the navigation never happens.

Wait on the registration itself, as the sibling leaveGuard-13163 save case
already does: a cancelable beforeunload that is no longer prevented means
nothing is registered. Assertions unchanged.

Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui seat 2 · session_01SMps7wq7rBTk53e8KTSEup · 2026-10-26T15:50Z · head 9c7d1a00

  1. 形态 (shape): a draft PR into main, assigned to os-steve and labelled skip-changeset, since the diff is test-only. The body leads with Fixes #26096. It touches one file, the claimed SettingsView.shellLeave-13231.test.tsx, with +16/−0.
  2. 前提实测 (premise, measured): the seat read the diff at this head.
    • The added wait sits before the after-save Profile click at :195. It waits on the fact the case asserts: a cancelable beforeunload is no longer prevented, so the page's unsaved-edits registration has been released.
    • The same pattern already exists on main in the sibling SettingsView.leaveGuard-13163.test.tsx (:243, :254, :277, :330). The assertions are unchanged.
    • There is no timeout, sleep, retry, skip or product change.
    • The dev's reverse proof:
      • Unfixed with an emulated starved worker: 2/2 failed, with confirmCalls=1 and the CI signature "NOT FOUND after ~1000 ms".
      • Fixed under the same emulation: 3/3 green.
      • Whole @object-ui/console project: 247 files, 2488 tests passed.
  3. 门禁 check-runs (gates): CI was in progress when I read it. Landing waits until every check is green.
  4. 接受的偏差 (accepted deviation): the claim's suggested route, sizing the findByText timeout and also touching :162 and :184, is not taken. This deviation is accepted, because the measured mechanism is a swallowed click: the route never changes, so no timeout could cure it. ui/AGENTS.md (测试纪律) also forbids widening timeouts. A probe showed :162 and :184 cannot meet the race, so leaving them untouched is correct.
  5. 范围外去向 (out of scope): the triage reading "timing cliff, 1066 ms vs 1000 ms" was a misread. It is recorded in the PR's acceptance notes, and no card is filed, because this is not a defect class.
  6. 落地条款 (landing terms):
    • No governed path (check-governed-merges: NOT governed).
    • apps/console is outside the registry closure, so no sdui record moves and the PR lands outside the package lines.
    • Lands via relay pr_ready + automerge_enable once green against the latest main.
    • If the only later change is a merge of main, this ACCEPT carries over.

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

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Queue-flake anchor: src/pages/settings/__tests__/SettingsView.shellLeave-13231.test.tsx

2 participants