Fix the access spec's setAccess flake, and stop settings flashing "denied" #244
No reviewers
Labels
No labels
No milestone
No project
No assignees
5 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/Ponente!244
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/access-spec-setaccess-flake"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
setAccessintests/e2e/access.spec.jsflaked in full runs ("a restricted board keeps its admin…", "the last admin cannot be removed"): 15 s timeout with Save access still disabled.Cause (reproduced)
On an isolated, port-shifted stack, a MutationObserver on the access page shows the same sequence on every visit:
The settings page re-renders from scratch when the circuit attaches, before its board has resolved, and
SettingsFrametreated "not resolved yet" as "denied".setAccessread the form with one-shotisChecked()/count()calls to decide whether a save was needed. Acount()landing in that gap saw no grant row (logged in 3 of 60 unthrottled calls), so the helper went on to "set" access the board already had. Save then never enables.With the db container CPU-throttled to stretch the gap, a second race showed up. After
save.click(),setAccesswaited for Save to go disabled, but Save is also disabled mid-save (_busy). The next page load read the old grants, and the super-admin test failed on "last admin".Fix
alreadyread.setAccessnow only asks for changes it can see land. It waits for the unsaved hint (.admin-hint) to go rather than for Save to disable. The admin tick gets its own short timeout, because its box stays disabled until the circuit has the grant, andtoPassnever interrupts a stuck attempt.SettingsFrameshows the loading spinner until the board resolves, asBoardPagealready does. "Denied" no longer flashes for the board's own admin (85–570 ms under the throttled db), and an e2edata-outcome="denied"assertion can't pass on the transient. No changelog line: on a healthy db the flash lasts one round trip, so few members will notice it gone.boards-and-permissions.mdno longer says Save disabling is how tests know a write landed.testing.mdgains the one-shot-read trap.Verification
Isolated stack (
ponenteflake, ports 1809x):access.spec.js --repeat-each=10: 50/50 unthrottled, 50/50 with db at 0.25 CPU (the old helper failed once under this setting), 50/50 at 0.1 CPU.search.spec.js"search finds a card…": amodal-backdrop-dockablecovers the search box. It never opens a settings page and passes alone, so it's a separate order-dependent flake and isn't touched here.tools/verify.sh: build + 584 unit tests green.🤖 Generated with Claude Code
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
The
SettingsFramefix is sound.BoardOutcome.Foundis the enum default, so every settings page starts outBoard == null && Outcome == Foundbefore it resolves. That is exactly the window the spinner now covers. TheMissing,DeniedandDeletedoutcomes, which resolve withBoard == null, still fall through to the existing branches. Theboard-loadingmarkup matches whatBoardPageuses, and the docs are updated alongside it.The spec changes are also reasonable.
setAccessno longer reads one-shot state. It waits on the unsaved hint rather than on Save disabling, which also happens mid-save. The two removed no-opsetAccesscalls rely on a new board being restricted to its maker, which the comment and docs state.Two minor notes, neither blocking:
search.spec.jsflake is correctly left out of scope.🧠 claude-sonnet-5-5 · ⏱ 0m 10s · 5 turns · tokens: 17.0k in / 983 out (+92.1k cache-read)
📜 Full session transcript
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
✅ Verdict: approve — no blocking issues found.
The fix is sound and well-reasoned.
App change: The spinner condition in
SettingsFrameis correct.BoardResolutiondistinguishesFound/Deleted/Missing/Denied, andSettingsPage._outcomestarts asFoundwith_boardnull, soBoard is null && Outcome == Foundcan only mean "not resolved yet" — afterResolveAsync, a null board always comes with a non-Foundoutcome, so no page can spin forever on a deleted/missing/denied board. The markup reuses the existing.board-loading/.spinnerpattern used byBoardPageand every other settings page. Since the child content only renders in theCanManagebranch,SettingsTabs'Board!.Keycan't deref null.Spec change: The
alreadyremoval is safe — all four remainingsetAccesscalls request a state that differs from what the board has, so Save always enables (and the docstring now states the precondition explicitly). Waiting on.access-save .admin-hintinstead of Save-disabled correctly skips the_busywindow (Save disables both mid-save and post-write, the hint only goes whenDirtyturns false). If a save is refused,Dirtystays true and the hint stays, so the helper still fails loudly rather than passing vacuously. The 1 s timeout on the admin tick'scheck()is justified: the box isdisableduntil its grant is ticked.expectDenied'stoBeVisibleretries past the new transient spinner, and the same is true for the denied assertion afteruntickAndSavein the super-admin test.The doc updates match the new behavior. The separate
search.spec.jsflake being out of scope is reasonable given it doesn't touch settings and passes alone.⏱ 1m 33s · 9 steps · $0.0120 · tokens: 59.4k in / 967 out (+55.9k cache-read) · Go: 5h 4% · wk 9% · mo 87%
📜 Full session transcript