Repository navigation
fix(live): restore the Multibyte only toggle before init awaits - #88
Merged
adminopenclaw8-sketch merged 5 commits intoSep 24, 2026
Merged
Conversation
Live init renders its controls, then awaits /api/config/map and loadNodes(). The "Multibyte only" box got its saved state and change listener only after both awaits. The feed, though, already filtered on the saved value from script load. While init ran, the box was visible and clickable but showed OFF even when saved ON. A click in that window was neither saved nor applied, and init reset the box when it caught up. The reload check in test-live-multibyte-only-e2e.js read the box as soon as it existed, so it failed whenever init was slow (CI serves instrumented JS). The fix moves the existing restore and listener to straight after the controls are rendered, before the first await. Nothing else changes: storage key, default and filter behaviour are the same. Each mount renders a new element, so the listener is still attached once. The test now holds /api/config/map to freeze init at its first await, and checks the box while init is held. It covers the saved true, false and unset states, a click during init, a real reload, and three SPA round trips with a single write per click. It then waits for Live's packet subscription (window._liveWSHandler()) and fails on page errors, console errors or unhandled rejections. It uses no sleeps or retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pendent Fixes for the independent review's findings: - Only console errors from the app's own origin count. External map tile failures (19 "Failed to load resource" errors when the tile CDN answers 503) made the error gate flaky in CI. Page errors, unhandled rejections and same-origin console errors still fail the test. - The packet feed waits now throw a clear message instead of being swallowed. - Each persistence step sets the saved value it needs, so one failure no longer cascades into later setup failures. - The reload step re-checks that the click from the previous step survived after that step's init settled. The comment notes that a revert deferred by an arbitrary timer is out of reach for a deterministic test. - Fresh-context steps close their context in a finally block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on setup Fixes for the re-review's two low findings: - Removed the carried-over re-check in the reload step. It ran with no gap after the previous step's own read, so it could not catch anything. The click step now says plainly that "kept" means kept after init finished, and that a timer-deferred revert is out of reach for a deterministic test. - The SPA step's setup reload now waits for Live init without asserting mid-init. Its round-trip and single-listener checks therefore always run. With master's live.js the step now fails on the round-trip check itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that the test checked only the checkbox and localStorage. Two realistic regressions passed: - rA: the internal filter no longer follows the saved value, while the box still shows it. - rD: a click during init is saved, but the filter is applied only after init. probeFeed() buffers one 1-byte and one 2-byte path-hash packet through window._liveBufferPacket and reads the rendered feed in the same evaluate. In LIVE mode, bufferPacket -> renderPacketTree -> addFeedItem is synchronous, so nothing waits. assertFeedFilter() requires three things: - the 2-byte packet renders, so the feed was live; - the 1-byte packet is hidden or shown exactly as the setting says; - both packets have the expected hash sizes. Each of these steps now probes the feed while init is held and after it finishes: the click during init, the reload with saved ON, the three fresh loads, and the SPA round trips plus the OFF click that follows. The probe header byte changes from 0x10 (transport flood, ADVERT) to 0x15 (FLOOD, GRP_TXT) to match route_type 1 and payloadTypeName. rA, both rD variants and an SPA state-loss mutant now fail on "setting is ON but the feed filter behaves as OFF". public/live.js is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #86 (Relay Airtime Share), #87 (blacklist QA hardening) and #90 (Reach Rank test stabilisation). None of them touch public/live.js or test-live-multibyte-only-e2e.js. The merge was conflict-free, and its tree equals the verified synthetic merge tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 25, 2026
This was referenced Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This fixes a race on the Live page's Multibyte only toggle. It was a real bug users could hit, not just a flaky test: while Live was starting up, the box showed the wrong state, and a click made then was lost.
test-live-multibyte-only-e2e.jscaught it by chance. CI run 35874411524 failed in its reload step and passed on one re-run.The race
public/live.json master does this. Line numbers are from4f59b993;live.jsis unchanged on the current master08716f43:multibyteOnlyis read fromlocalStorage['live-multibyte-only']. Feed filtering is therefore correct from the start.init()renders the controls withapp.innerHTML.#liveMultibyteTogglehas nocheckedattribute, so it renders OFF.init()then awaitsfetch('/api/config/map')(line 1283) andloadNodes()(line 1570).multibyteToggle.checked = multibyteOnlyand attach thechangelistener.Between steps 2 and 4 the controls panel is open, and the box is visible and clickable. Holding
/api/config/mapwith Playwright request interception reproduces this deterministically on master:stored=null)trueat onceWithout the hold, the window was about 30–35 ms locally. It grows with network latency and with the node count behind
loadNodes(). CI serves Istanbul-instrumented JS, which makes it slower still, and that is where the test failed.Reload and SPA hash navigation both go through
registerPage('live').init→init(app), so both were affected.Fix
Production: in
public/live.js, the existing 6-line restore-and-listener block now runs right afterapp.innerHTML, before the first await. The code itself is unchanged, and so are the storage key, the default and the filter behaviour.Every mount renders a new element, so the listener is still attached exactly once.
rebuildFeedList()was already callable during init throughtheme-refresh. It returns early without#liveFeedand touches neither the map nor nodes.Test:
test-live-multibyte-only-e2e.jskeeps its place in the CI sequence. There is no new registration.Why the new test is deterministic
/api/config/map, the first await in Live init, usingpage.route(..., { times: 1 }). That freezes init right after the controls are rendered, and all toggle checks run inside that window.window._liveWSHandler()returns a handler.connectWS()sets it afterloadNodes(), in the same synchronous run of init as the old toggle wiring.destroy()clears it, so it marks a finished init on every load and SPA mount.Coverage:
true,falseand unset, each shown correctly while init is held (savedtruecannot pass on the OFF default);_liveBufferPacketboth while init is held and after it, and the test checks which one renders (see "Feed-filter assertions" below);page.reload();#/packetsand back, with the setting kept and exactly one write per click (no piled-up listeners);Evidence
All results are fresh from this branch on macOS arm64 with Playwright 1.58.2 and Chromium 1208. The server is the local Go server with the CI fixture (freshen → CI seed SQL → migrate). No staging, demo or production system was contacted.
Target test
b8c1d719).Red on master: the new test fails with master's
live.json "a click during init must save the setting at once (stored=false)" and "toggle must show ON while init is still running, got OFF".Mutations (each fails 3/3 with the intended message; M1–M7 were re-run after each review round):
live.js(fix reverted)_liveWSHandleralways nullwrites=2checked/api/config/mapconsole.errorduring initOther checks
node --checkpasses on both changed files, andgit diff --checkis clean.deploy.yml.scripts/check-xss-sinks.sh --diff origin/master: clean.eslint public/*.js: 0 errors.live.jshas 13 warnings on both master and this branch.CI-like Playwright sequence: all 109 commands of "Run Playwright E2E tests" and the slide-over gate, in CI order, against the instrumented build.
/usr/bin/chromium. With a fresh fixture andCHROMIUM_PATHset, they pass on both master and this branch.test-reach-rank-e2e.jswas flaky on master at the time: 2/8 failures on master, 0/8 on this branch. That flake is now fixed on master by test(reach-rank): wait for the remounted board before typing; prove Back restores the search #90, and this branch includes it (see "Synced with master").Browser check: Chromium with real clicks, at 1400×900 desktop and 375×812 mobile (touch, mobile UA). Covered: the default, a click, a reload with a frame taken mid-init (the box already shows the saved ON), SPA away and back, and console errors (none).
Feed-filter assertions (merge-review finding,
b8c1d719)A later merge review found that the test proved the checkbox, localStorage and readiness, but not that the setting filters the feed. Two realistic regressions passed:
The commit is test-only;
public/live.jsis byte-identical toa786621e.How the probe works:
probeFeed()buffers one packet with a 1-byte and one with a 2-byte path hash throughwindow._liveBufferPacket, then reads the rendered feed DOM in the sameevaluate. In LIVE mode,bufferPacket→renderPacketTree→addFeedItemis synchronous, so nothing waits and nothing retries.assertFeedFilter()requires three things:MC_packetHashSizereports sizes 1 and 2.Where it probes, both while init is held and after it:
true,falseand unset;Probe header byte: it changes from
0x10to0x15.0x10is route00(transport flood) with payload4(ADVERT).0x15is route01(FLOOD) with payload5(GRP_TXT), which matchesmakePkt'sroute_type: 1andGRP_TXT(firmware/docs/packet_format.md).…00gives 1 and…40gives 2.Mutations (isolated copies of
public/, each run 2×):loadNodes()destroy()drops the filter state, and the box is set from storagelive.jsconsole.errorFresh verification of
b8c1d719(macOS arm64, local Go server, CI fixture; no staging or production):node --check,git diff --check, and the XSS diff gate are clean.live.jshas 13 warnings on both this branch and mastere51272d9.test-frontend-helpers.js,test-issue-1485-live-anim-z.js,test-live-anims.jsandtest-panel-corner.js. They fail identically on a cleane51272d9checkout and are not run in CI.A fresh reviewer that did not write this change reviewed the test-only patch. Its first round flagged the missing SPA feed probe (rS) and the header byte; both are fixed. Its re-check found no blockers.
Independent review
A separate reviewer agent that did not write the change confirmed the race, the init ordering and the readiness signal. It built its own mutants and found no blockers in either round.
bfbcd861:finally.a786621e:/api/*failures and the app's ownconsole.errorcalls are still counted; only external CDN resource errors are dropped.Synced with master (
75f90845)The branch was brought up to date with a regular merge commit,
75f90845. Its parents are the previous headb8c1d719and master08716f43; there was no rebase, squash or force. The merge was conflict-free, and its treedf269886equals the synthetic merge tree that was verified before the sync.Master now includes:
None of them touch
public/live.js,test-live-multibyte-only-e2e.jsor the Live init, toggle, feed or WebSocket code. The diff against master is still exactly those two files, with this PR's blobs, and the multibyte test is registered once indeploy.yml.Fresh verification on the merge tree (local Go server, CI fixture; no staging or production):
Multibyte test: 10/10 plain and 5/5 instrumented. Every run holds init, so every run is a controlled slow-init run.
Mutants, each run 2× and failing on its intended assertion:
writes=2;console.errorfails on the error gate.The external tile-503 probe stays green, 2/2.
Live E2E: all 14 other tests pass.
Reach Rank (test(reach-rank): wait for the remounted board before typing; prove Back restores the search #90):
test-reach-rank-e2e.jspassed 10/10, with the Back/Forward search step green every time. CI positions 91–97 passed 3/3 per test.Static and workflow checks:
node --check, eslint 8 (13live.jswarnings, as on master), the XSS diff gate andgit diff --checkare all clean.Browser check: Chromium at 1400 px desktop and 375 px mobile passed: real click, reload with init held, and SPA away and back, with no page, window or console errors and no unhandled rejections.
Independent delta review: a fresh reviewer that did not write this PR or the sync found no blockers.
live.jschange is still only the move of restore and listener before the first await, and that the fork guards are intact.loadNodes(), and init snapshotting the setting at its start and reapplying it afterloadNodes().Known limitations
Map container not found(oralready initializedafter a quick return). It is not changed here.Files
public/live.js: moves the restore and listener block (+13/−8)test-live-multibyte-only-e2e.js: deterministic persistence, race and feed-filter coverage🤖 Generated with Claude Code