Skip to content

fix(ui): follow-ups to #255 — mobile group dead end, nodesEsc leak, test gaps (#259) - #260

Merged
dborup merged 9 commits into
masterfrom
codex/issue-259-followups-255
Oct 6, 2026
Merged

dborup merged 9 commits into
masterfrom
codex/issue-259-followups-255

Conversation

@dborup

@dborup dborup commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Relates to #259

Follow-ups from the review of #255 (#254, merged as 3bb2cb89). Items 1 and 2 are pre-existing behaviour that #255 did not cause; items 3 and 4 close two gaps in #255's own tests.

1. An expanded group no longer leaves orphan children on mobile

A group expanded at desktop width kept class="expanded" and its child rows after the layout crossed to ≤ 600 px. There the expand column is hidden and the row only selects (Kpa-clawbot#1461 #7, #255), so nothing was left to collapse it — the dead end upstream #1461 #7 describes, and finding F2 of the #255 review.

Chosen option: keep the hash in expandedHashes and leave the children out of the rendered slice while the mobile mode is active, rather than clearing the set on the flip. Two reasons:

  • the 600 px line is crossed in both directions by a phone rotating (390×844 is mobile, 844×390 is not), so clearing would discard the user's expansion on every rotation; suppressing the rows restores it on the way back;
  • the dead end also appears on a first render at a narrow width — the bug: /#/packets/<hash> full-page — clicking a different observation doesn't update hex payload or path details Kpa-clawbot/CoreScope#866 deep link #/packets/<hash>?obs=<id> adds the hash to expandedHashes before any row is built — which a mode-flip hook alone would not catch. That first-render case is covered by the test-packets.js unit cases, which seed expandedHashes and render at 390 px; the E2E's 390 px touch context is reached by ?hash=, which only filters and expands nothing.

One render-time helper, groupIsExpandedInView(), is used by both buildGroupRowHtml and _getRowCount, so the virtual-scroll row counts and the rendered rows cannot diverge (Kpa-clawbot#424). The mode-flip resize handler now also invalidates the cached counts, because an expanded group's row count changes with the mode.

Desktop rendering is untouched: carets, aria-expanded and expandedHashes behave exactly as in #248/#255, over live updates, sorting and filtering.

2. One Escape listener per node page, not one per visit

nodes.js init() added a fresh nodesEsc closure to document on every full-screen node-page init and only unhooked it when Escape actually fired, so node A → B → C left three live listeners and one Escape wrote the same hash three times (finding F4).

The handler is now a single module-level _nodesEsc — so a repeat addEventListener is a DOM no-op — and destroy() removes it, which covers both the router's destroy/init cycle and the in-module navigateToNode path.

The nodes list view had the same leak in nodesPanelEsc (Escape closes the detail panel), registered inside renderLeft(). renderLeft() runs on every load of the list — a visit, a region change, a filter change — so the listeners stacked there too, and nothing ever removed them. Fixed the same way, as _nodesPanelEsc (finding F3 of the #260 review; pre-existing, but small and in the same file).

3. Test gap F1: the fast layer now proves the node page uses the renderer

test-issue-254-affinity-debug-toggle.js exercised renderAffinityDebugCard() in isolation, so the review's mutant — master's inline onclick card back in the loadFullNode template, the renderer exported but unused — passed it 9/9 and only the E2E caught it. Three source assertions close that: the template interpolates ${renderAffinityDebugCard()}, id="node-affinity-debug" occurs exactly once in public/nodes.js, and no inline on*= handler sits anywhere near the card. That mutant now fails the unit test 3 cases.

4. Test gap F3: Space on a mobile group row

The #254 E2E pressed Enter on the 390 px group row but not Space, although the handler treats them the same. A Space step was added: the detail sheet opens, the group does not expand, and the row is still select-hash without aria-expanded.

Item 1's render rule makes the "does not expand" half invisible in the DOM: at ≤ 600 px a hash that is in expandedHashes renders exactly like a collapsed row. So the 390 px tap, Enter and Space steps now also read expandedHashes itself, through a new _isExpanded(hash) hook on _packetsTestAPI, and both mobile E2Es resize back to 1400 px afterwards and assert the group is still collapsed — the render-level view, and what a user rotating a phone to landscape would see (finding F1 of the #260 review).

Tests

Layer File New
unit test-packets.js 7 cases for an expanded group across the breakpoint (render, _getRowCount, 600/601 px, state survives the round trip)
unit test-issue-259-nodes-esc-listener.js (new, registered in test-all.sh) 11 cases counting the document keydown listeners and the hash writes — A → B → C for the full-screen view, and the list view over a region change and five router cycles
unit test-issue-254-affinity-debug-toggle.js 3 source assertions (F1)
E2E test-issue-259-mobile-expanded-group-e2e.js (new, registered in deploy.yml next to the #254 line) 1400 → 390 → 1400 in light and dark, plus a 390 px touch context that also goes back over the breakpoint
E2E test-issue-254-affinity-toggle-mobile-aria-e2e.js Space on the 390 px group row (F3), expandedHashes assertions on tap/Enter/Space, and a 390 → 1400 round trip

_setDisplayGrouped and _isExpanded join the existing _packetsTestAPI hooks: the first so the unit sandbox can exercise _getRowCount in grouped mode, the second so the mobile E2E steps can see an expansion the mobile render deliberately hides.

The mutants, the suite results and the screenshots are in the report comment below.

Not changed

🤖 Generated with Claude Code

dborup and others added 4 commits October 5, 2026 16:03
Tests first, red on master 3bb2cb8 where they describe a behaviour change.

1. test-packets.js: an expanded group across the 600 px breakpoint. At 390 px
   it must render no child rows, no `expanded` class and no down caret, while
   the hash stays in expandedHashes so the children return at 1400 px.
   _getRowCount must agree with the rendered rows on both sides (Kpa-clawbot#424).
   4 of the 7 new cases fail on master.

2. test-issue-259-nodes-esc-listener.js: the node page's document keydown
   handler (`nodesEsc`). After the router's destroy/init cycle for node
   A -> B -> C at most one listener may survive, Escape may write the hash
   once, and destroy() must leave none behind. 4 of 7 fail on master, where
   three listeners stack and Escape navigates three times.

3. test-issue-254-affinity-debug-toggle.js: three source assertions so the
   fast layer proves the loadFullNode template goes through
   renderAffinityDebugCard() and holds no inline on*= handler. Green on
   master by design — these close review finding F1, where the mutant that
   restores the old inline card passed the unit test 9/9.

4. test-issue-254-affinity-toggle-mobile-aria-e2e.js: press Space, not only
   Enter, on the 390 px group row (review finding F3).

New E2E test-issue-259-mobile-expanded-group-e2e.js registered in deploy.yml
next to the #254 line; the new unit file registered in test-all.sh.

Relates to #259

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…259)

A group expanded at desktop width kept `class="expanded"` and its child rows
after the layout crossed to <= 600 px. There the expand column is hidden and
the row only selects (Kpa-clawbot#1461 #7, #255), so nothing was left to collapse it: the
dead end upstream Kpa-clawbot#1461 #7 describes, and finding F2 of the #255 review.

Keep the hash in `expandedHashes` and leave the children out of the rendered
slice while the mobile mode is active, rather than clearing the set on the
flip. Two reasons:

- the 600 px line is crossed in both directions by a phone rotating
  (390x844 is mobile, 844x390 is not), so clearing would discard the user's
  expansion on every rotation; suppressing the rows restores it on the way
  back;
- the dead end also appears on a *first* render at a narrow width — the Kpa-clawbot#866
  deep link #/packets/<hash>/<obs> adds the hash to `expandedHashes` before
  any row is built — which a mode-flip hook alone would not catch.

One render-time helper, `groupIsExpandedInView()`, is used by both
`buildGroupRowHtml` and `_getRowCount`, so the virtual-scroll row counts and
the rendered rows cannot diverge (Kpa-clawbot#424). The mode-flip resize handler now also
invalidates the cached counts, because an expanded group's row count changes
with the mode. Desktop rendering is untouched: carets, `aria-expanded` and
`expandedHashes` behave exactly as in #248/#255.

`_setDisplayGrouped` joins the existing test hooks so the unit sandbox can
exercise `_getRowCount` in grouped mode.

Relates to #259

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`init()` added a fresh `nodesEsc` closure to `document` on every full-screen
node-page init and only unhooked it when Escape actually fired. Navigating
node A -> B -> C therefore left three live listeners, and one Escape wrote the
same hash three times. Finding F4 of the #255 review.

The handler is now a single module-level `_nodesEsc`, so a repeat
`addEventListener` is a DOM no-op, and `destroy()` removes it — which covers
the router's destroy/init cycle and the in-module `navigateToNode` path.

Relates to #259

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The counting document.addEventListener in the new test pushed every
registration, so it reported three listeners even for a stable handler
reference that the real DOM would have registered once. It now deduplicates on
(type, handler), which makes the count the listener count rather than the
number of add() calls.

Both mutants are still killed: master's per-init closure stacks three
listeners, and keeping the stable reference while dropping the destroy()
removal fails the "destroy() removes the Escape listener" case.

Relates to #259

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dborup

dborup commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Rapport — CS-Minimax PR#260 #259 — head 0ed1a3d

Status: All four items of #259 are implemented on a draft PR; every item has a test and a mutant, the local suites are green, and the PR stays a draft.

Evidence tags: [T] test or run output, [A] assessment or inference, [K] checked in code, diff, git or CI.

Branch

codex/issue-259-followups-255 from origin/master 3bb2cb89, in a dedicated worktree, four commits, tests first. All have author and committer dborup <kontakt@meshview.dk>. No rebase, amend or force-push; only explicit git add. [K]

  1. 4d63141c tests (red on master where they describe a behaviour change).
  2. ffd21133 the packets fix (item 1).
  3. fdb5fe6f the nodes fix (item 2).
  4. 0ed1a3df the new test sandbox deduplicates listeners the way the DOM does, so its count is a listener count rather than a count of addEventListener calls.

The plan is on #259. No closing keywords in the title, body or commit messages; closingIssuesReferences is empty. [K]

Requirements

# Requirement Test Mutant Result
1 A group expanded on desktop leaves no visible children, and no dead end, at ≤ 600 px test-packets.js 7 new cases (render, _getRowCount, 600/601 px, round trip); E2E test-issue-259-mobile-expanded-group-e2e.js 1400 → 390 → 1400 in light and dark plus a 390 px touch context M1 ✅ [T]
1 Desktop behaviour from #248/#255 unchanged (carets, aria-expanded, expandedHashes) test-packets.js 1400 px and 601 px cases; test-issue-189-group-caret-e2e.js 3/3; the #254 E2E's own desktop and resize steps M1 ✅ [T]
1 The expansion is kept, not cleared, across the flip test-packets.js "survives 1400 → 390 → 1400"; E2E step 3 asserts aria-expanded="true" and the children are back M1 ✅ [T]
2 At most one document keydown listener per node page test-issue-259-nodes-esc-listener.js: A → B → C counts listeners and Escape hash writes; destroy() leaves none M2, M2b ✅ [T]
3 Fast-layer assertion that the loadFullNode template uses renderAffinityDebugCard() and the card has no onclick= test-issue-254-affinity-debug-toggle.js, 3 source assertions M3 ✅ [T]
4 Space on a 390 px group row opens the sheet and does not expand test-issue-254-affinity-toggle-mobile-aria-e2e.js, new Space step M4 ✅ [T]
— No hardcoded colours the diff adds no hex, rgb() or hsl() value — ✅ [K]
— No per-item API calls the change is render-time only; no api() call added — ✅ [K]
— scripts/check-xss-sinks.sh --diff origin/master clean exit 0, no output — ✅ [T]
— Fork guards unchanged 9 in deploy.yml, 1 in release-fast-path.yml — ✅ [K]
— New E2E registered next to the #254 line one added line in deploy.yml, directly after test-issue-254-affinity-toggle-mobile-aria-e2e.js; new unit file in test-all.sh; test-test-all.js 10/10 — ✅ [K][T]

Why option 2 for item 1

The issue offered clearing expandedHashes on the flip to mobile, or keeping the state and omitting the children from the visible slice. The second was chosen. [A]

One helper, groupIsExpandedInView(), is used by both buildGroupRowHtml and _getRowCount, so the rendered rows and the virtual-scroll row counts cannot diverge (Kpa-clawbot#424). The mode-flip resize handler also invalidates the cached counts. [K]

Red on master 3bb2cb89 [T]

Test On master On the branch
test-packets.js (#259 block) 142 passed, 4 failed 146 passed, 0 failed
test-issue-259-nodes-esc-listener.js 3 passed, 4 failed (three stacked listeners, Escape navigated three times) 7 passed, 0 failed
test-issue-259-mobile-expanded-group-e2e.js see M1 below — 2 failed, the dead end reproduced 9 passed, 0 failed
the 3 new F1 source assertions green on master by design — they close a mutant gap, not a current bug; M3 is their proof green
the new Space step (F3) green on master — the handler already accepts Space; M4 is its proof green

Mutants [T]

Each ran against the relevant layers, with a server restart per mutant that needed one, then git checkout -- public/ and a clean tree.

Mutant What test-packets.js nodes-esc unit #254 unit #259 E2E #254 E2E Verdict
M1 item 1 reverted: buildGroupRowHtml and _getRowCount back to the plain expandedHashes.has() check (master) 4 failed — — 2 failed — killed
M1b the _invalidateRowCounts() call dropped from the mode-flip handler green — — green — survives, see below
M2 item 2 reverted: per-init closure, no removal in destroy() (master) — 4 failed — — — killed
M2b stable handler reference kept, but destroy() no longer removes it — 1 failed — — — killed
M3 master's inline onclick card back in the loadFullNode template, renderer exported but unused (the review's M1) — — 3 failed — — killed, now by the fast layer
M4 ' ' dropped from the row keydown branch in packets.js — — — — 1 failed (only the new Space step; Enter still passed) killed

M1's E2E failure is the dead end itself, verbatim from the run:
{"action":"select-hash","aria":null,"expandedClass":true,"children":3,"visibleChildren":3,"expandCellVisible":false,"carets":["ph-caret-down"]} — three child rows on screen, the row still marked expanded, and the expand column hidden. [T]

M1b survives and is reported as such. Dropping _invalidateRowCounts() leaves the cached per-entry row counts stale after a mode flip, which affects only the virtual-scroll spacer heights and the incremental row-removal arithmetic for a long, scrolled list. Every layer here reaches the seeded group through ?hash=…, so the table holds a single entry and never scrolls, and the rendered rows are correct either way. The call is kept because the stale count is a real inconsistency, but no test observes it. [T][A]

Suites (local Go server on a copy of e2e-fixture.db, prepared as in CI: freshen, the deploy.yml seed SQL, corescope-migrate, seeds 2073 and 199; a free local port, stopped by port) [T]

Suite Result
sh test-all.sh 220/220 files
node test-frontend-helpers.js 707 passed, 0 failed
node test-packets.js 146 passed, 0 failed
node test-issue-254-affinity-debug-toggle.js 12/12
node test-issue-259-nodes-esc-listener.js 7/7
node test-issue-259-mobile-expanded-group-e2e.js (new) 9/9
node test-issue-254-affinity-toggle-mobile-aria-e2e.js 13/13 (12 before, plus Space)
node test-issue-189-group-caret-e2e.js 3/3
node test-issue-1461-mobile-page-actions.js 6/6
node test-test-all.js 10/10

Screenshots (local Chromium; described, not attached — the CLI cannot upload images) [T]

  • 1400 px, light, expanded: the seeded 3-observation group sits at the top of the Latest Packets table with a down caret in the expand column, an eye badge "3" in the RPT column and its three child rows below it (DUBLIN Obs, GY889 Repeater, Kennedy Repeater, hop chips cc000000 / bb000000 / aa000000). The detail panel on the right shows "Packets page collapse button in the left column of the table opens the dialog. Kpa-clawbot/CoreScope#1486 fixture · observation 1 of 3".
  • 1400 px, dark, expanded: identical layout on the dark card tokens; the same single down caret and the same three children. No colour value was added, so both themes render from the existing variables.
  • 390 px, light, after resizing from 1400 px: one row left — Time / Type / Details, no expand column, no child rows, and the mobile bottom nav below. The group row carries class="group-header " with no expanded, data-action="select-hash" and no aria-expanded.
  • 390 px, dark, after the same resize: the same single row on the dark background.
  • 390 px touch, detail sheet: a tap opens the bottom detail sheet ("Observations (3)") and the row behind it stays collapsed with no children.

CI [K][T]

Run on head 0ed1a3df, attempt 1, conclusion success. No reruns, and neither known flake fired (#250 TestStatsFileHasNoCredentials, #256 Hash Stats sort — the Hash Stats URL-state cases all passed).

Job Result Evidence from the log
✅ Go Build & Test success test-all.sh 220 passed, 0 failed (220 files), including test-issue-259-nodes-esc-listener.js; the PR-only XSS --diff origin/master preflight ran
✅ Playwright E2E Tests success new test-issue-259-mobile-expanded-group-e2e.js 9/9; test-issue-254-affinity-toggle-mobile-aria-e2e.js 13/13, Space step included; test-issue-189-group-caret-e2e.js 3/3
✅ Build & Publish Docker Image success —
📦 Release Artifacts skipped fork-guarded
🚀 Deploy Staging skipped fork-guarded
📝 Publish Badges & Summary skipped fork-guarded

Remainder

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent1 PR#260 — head 0ed1a3d

Dom: REQUEST CHANGES

Evidence tags: [T] test or run output, [A] assessment or inference, [K] checked in code, diff, git or CI.

The production changes for items 1 and 2 are correct. I checked them in a browser on the merged tree and with my own mutants. One test gap blocks the merge: item 1's render rule hides a group's expansion at ≤ 600 px, so every 390 px "does not expand" assertion can no longer see an expansion. That includes the new Space step for item 4 and the existing #254 tap and Enter steps. A mutant that silently toggles the group on mobile survives every layer on this branch, but the #254 E2E kills it on master. The fix is small and only touches tests (see F1).

Findings

# Severity Where Finding Evidence
F1 Medium (test, blocking) test-issue-254-affinity-toggle-mobile-aria-e2e.js:268, :281, :296; test-issue-259-mobile-expanded-group-e2e.js:165 With groupIsExpandedInView(), a hash in expandedHashes renders exactly like a collapsed row at 390 px: no expanded class, no children. The tap, Enter and Space steps check "does not expand" only through expanded and children, so they cannot fail on an expansion at that width anymore. Mutant RM5b (a select-hash group header calls pktToggleGroup(value) and then pktSelectHash(value), so the sheet opens and the group toggles unseen) passes test-packets.js 146/146, the #254 E2E 13/13, the #259 E2E 9/9 and #189 3/3 on the merged tree. The same mutant on master's packets.js fails the #254 E2E 2× (tap and Space: "expanded":true,"children":3). The PR therefore weakens two existing assertions and leaves the "does not expand" half of item 4 unproven. The user-visible effect of such a regression: tap a group on a phone, rotate to landscape, and the group is open. Suggested fix: after the 390 px tap/Enter/Space steps, resize to 1400 px and assert aria-expanded="false" with no children. Alternatively, read expandedHashes through a test hook. Either way RM5b must die on the branch. [T]
F2 Low (test) test-issue-259-nodes-esc-listener.js:182 "the nodes list view adds no Escape listener" passes for the wrong reason. The real list view does add a document keydown listener, nodesPanelEsc in renderLeft() (public/nodes.js:1679), but only after the async loadNodes() resolves. The sandbox asserts synchronously before that. The case name states something untrue about the page. Rename it, or await the load and count it. [K][T]
F3 Info (pre-existing, out of scope) public/nodes.js:1679 nodesPanelEsc has the same leak class as item 2: one listener per list-view render, never removed. In Chromium, after node A → list → node B → list there were 3 nodesPanelEsc listeners on document. Its effect is guarded (it acts only while a detail panel is open), so it is harmless today. #259 names only nodesEsc, so this is a candidate for a follow-up, not for this PR. [T][K]
F4 Nit (docs) PR body; test-issue-259-mobile-expanded-group-e2e.js:156-158 The Kpa-clawbot#866 deep link is #/packets/<hash>?obs=<id> (packets.js:1235), not #/packets/<hash>/<obs>. The touch step's comment says the group is "deep-linked and expanded by the URL", but it opens ?hash=…, which does not expand anything, so the first-render-at-narrow-width case is covered only by the unit cases. I checked it in a browser (below): it works. [K][T]
F5 Nit (test) test-packets.js:1326 On master this case is red only because the _setDisplayGrouped hook is missing (_getRowCount returns 1 at 1400 px), not because of the behaviour. On the branch it does kill RM1 (_getRowCount alone reverted), so it earns its place. [T]
F6 Info (pre-existing) packets page Not caused by this PR, and identical with master's packets.js: (a) a ?obs= deep link at 1400 px renders the header expanded with a down caret but 0 children until a later refresh; (b) the desktop detail pane opened by an expand stays over the list after a flip to 390 px (it can be closed with ×, so it is not a dead end). [T]

The author's M1b (the _invalidateRowCounts() call in the mode-flip handler is unobserved) is acknowledged, and I agree with keeping the call. [A]

The review points

  1. Dead end on mobile. Fixed. [T] With my own Playwright script on the merged tree, I expanded the seeded group at 1400 px (aria-expanded="true", 3 visible children, one ph-caret-down) and resized to 390 px. The result was select-hash, no aria-expanded, no expanded class, 0 children (none rendered at all) and a hidden expand cell. A tap opens the sheet and leaves 0 children. A live update at 390 px (a new observation inserted into the fixture DB and picked up by the poller) leaves 0 children. Back at 1400 px the group is expanded again with all children and the down caret. Desktop behaviour from fix(packets): collapsed groups show a right caret, expanded a down caret (#189) #248/fix(ui): Affinity Debug toggle and mobile group-row aria-expanded (#254) #255 is unchanged:

    • a live update at 1400 px adds a child (4 → 5);
    • sorting on Observer, ascending and descending, keeps the group expanded with one caret;
    • the filter type == GRP_TXT keeps it expanded, and a 390 → 1400 round trip under that filter behaves the same way;
    • the caret collapses it (aria-expanded="false", ph-caret-right);
    • there are no page errors.

    The ?obs= deep link that expands before the first render shows 0 children and no expanded class at 390 px (touch). On the master packets.js the same script reproduces the dead end: expanded class, 5 visible children, expand cell hidden, ph-caret-down. Light and dark: the PR E2E runs both themes, 9/9.

  2. nodesEsc. Fixed. [T] I counted document keydown listeners through CDP DOMDebugger.getEventListeners. The packets page has 5 as a baseline. After node A → B → C by hash navigation there are 6 in total, and exactly one comes from nodes.js (line 587, _nodesEsc). Escape goes to #/nodes with exactly one hashchange. The in-module navigateToNode() path also goes through destroy() [K]. See F3 for the sibling nodesPanelEsc.

  3. Test gap F1 of fix(ui): Affinity Debug toggle and mobile group-row aria-expanded (#254) #255. Closed. [T] I built RM4 independently: the exact inline-onclick card from 3bb2cb89^1 back in the loadFullNode template, with the renderer exported but unused. It fails test-issue-254-affinity-debug-toggle.js 3× (12 → 9 passing) and the Follow-up to #248: Affinity Debug toggle broken + reversed carets; group-row aria-expanded on mobile #254 E2E 5×.

  4. Test gap F3 of fix(ui): Affinity Debug toggle and mobile group-row aria-expanded (#254) #255. Half closed. [T] At 390 px, Space on the focused group row opens the detail sheet: the step passes, and the author's M4 (Space dropped) shows that it bites. The "does not expand" half cannot fail on this branch; see F1.

  5. Registration. OK. [K][T] deploy.yml gains exactly one line, the Follow-ups to #255: expanded group dead end on mobile, nodesEsc listener leak, two test gaps #259 E2E directly after the Follow-up to #248: Affinity Debug toggle broken + reversed carets; group-row aria-expanded on mobile #254 line, and test-all.sh gains the nodes-esc unit file. The Tufte v2: mobile packets view — kill chrome, kill duplication (follow-up to #1458 / #1459) Kpa-clawbot/CoreScope#1461 test is a unit file in test-all.sh and is not in deploy.yml. CI on this head (run 37323250705, attempt 1) shows:

    The test: 13 red orphan unit tests + 4 orphan E2E files left after #187; collapsed packet groups show an up-caret #189, Follow-up to #248: Affinity Debug toggle broken + reversed carets; group-row aria-expanded on mobile #254 and Tufte v2: mobile packets view — kill chrome, kill duplication (follow-up to #1458 / #1459) Kpa-clawbot/CoreScope#1461 tests are also green locally (below).

Always-checks

Check Result
Every AC has a test that is red before and green after Item 1: test-packets.js 4 red on master, green on the branch; my own browser script reproduces the dead end on master [T]. Item 2: nodes-esc unit 4 red on master [T]. Items 3/4 are test-only by design and are proven by mutants (RM4 [T]; author's M4). Except the "does not expand" half of item 4 (F1).
Own mutants (6) Table below [T]
No behaviour change beyond the purpose The production diff is limited to groupIsExpandedInView() with its two call sites, one _invalidateRowCounts(), a test hook, and the _nodesEsc refactor [K]. The existing fetch of /packets/<hash> for hidden expanded groups on mobile is unchanged, as the author says [K].
cmd/server read-only, no new map[string]interface{} No .go file touched [K]
No hardcoded colours No hex/rgb()/hsl() added [K]
scripts/check-xss-sinks.sh --diff origin/master exit 0, no output (scratch clone at head, origin/master = c6b356de) [T]
Fork guards 9 in deploy.yml, 1 in release-fast-path.yml, same as master [K]
Closing keywords none in title, body or commits; closingIssuesReferences empty [K]
Commit author/committer all four commits dborup <kontakt@meshview.dk> for both [K]

Mutants (mine, against the merged tree; server restarted and fixture re-copied per mutant) [T]

Mutant What test-packets.js nodes-esc unit #254 unit #259 E2E #254 E2E #189 E2E Verdict
RM1 only _getRowCount back to expandedHashes.has() 1 failed 7/7 12/12 9/9 13/13 3/3 killed (unit only)
RM2 only buildGroupRowHtml back to expandedHashes.has() 3 failed 7/7 12/12 2 failed (dead end, light and dark) 13/13 3/3 killed
RM3 init() registers a fresh wrapper function (e) { _nodesEsc(e); }, destroy() unchanged 146/146 4 failed 12/12 9/9 13/13 3/3 killed
RM4 the #255 F1 mutant: old inline-onclick card in the template, renderer unused 146/146 7/7 3 failed 9/9 5 failed 3/3 killed by the fast layer
RM5 a select-hash group header calls pktToggleGroup instead of pktSelectHash 146/146 7/7 12/12 9/9 1 failed (Enter: sheet did not open, because the second toggle collapsed) 3/3 killed by accident
RM5b as RM5 plus pktSelectHash afterwards (sheet always opens) 146/146 7/7 12/12 9/9 13/13 3/3 survives (F1); the same mutant on master's packets.js fails the #254 E2E 2×

Tests run (merged tree git merge-tree --write-tree origin/master 0ed1a3df… on origin/master c6b356de, clean merge) [T]

Suite Result
sh test-all.sh 220/220 files on a serial run. A first run, in parallel with both Go suites, had 1 failure in test-channels-client-state-152.js (R4-3 S2, a timing case). It was 67/67 on 3 isolated reruns and on master; channels.js is untouched.
node test-frontend-helpers.js 707 passed, 0 failed
cmd/server go test ./... ok (912 s, -timeout 20m). A first parallel run hit the default 10 min timeout under load.
cmd/ingestor go test ./... ok (973 s, -timeout 20m). Same as above for the first run.
node test-packets.js / nodes-esc / #254 unit 146/146, 7/7, 12/12
E2E against a local Go server on e2e-fixture.db, prepared as in CI (freshen, the deploy.yml seed SQL, corescope-migrate, seeds 2073, 199 and 245), stopped by port #259 9/9, #254 13/13, #189 3/3, Kpa-clawbot#1461 6/6
Own browser script (above) 18/18 on the branch's packets.js

CI on head 0ed1a3df: run 37323250705, attempt 1, success. Go Build & Test ✅, Playwright E2E ✅, Docker ✅; Release, Deploy Staging and Badges skipped (fork-guarded). Neither known flake fired: the Hash Stats sort (#256) and the backfill write-hold (#267) did not fail. [K]

Not verified

  • Firefox, WebKit/Safari, a real phone, a real screen reader. [A]
  • The -race variant of the Go server suite that CI runs; I ran it without -race. No Go code changed. [A]
  • CI on the merged tree against the current origin/master (c6b356de). Only my local runs cover that. [A]
  • Staging and production, by design.
  • Screenshots were taken locally and inspected, not attached.

Head was 0ed1a3df65575fc08b2aa58d9b69ca9623a72bf5 on git ls-remote before and after the review. Nothing was pushed or changed on the PR.

dborup added 4 commits October 5, 2026 21:25
Review F1 on #260: since groupIsExpandedInView(), a hash that *is* in
expandedHashes renders at <= 600 px exactly like a collapsed row -- no
`expanded` class, no child rows. The 390 px tap, Enter and Space steps
asserted only on those two, so they could no longer fail on an expansion
at that width: a mutant whose select-hash row toggles the group *and*
opens the sheet passed every layer on this branch, while the same mutant
on master's packets.js failed the #254 E2E twice.

Two independent ways to see it again:

- packets.js exports `_isExpanded(hash)` on the existing test API, so the
  steps read expandedHashes itself. A missing hook yields the string
  'NO-HOOK', which fails the assertion rather than passing it.
- both E2Es now resize back to 1400 px after the mobile activations and
  assert the group is still collapsed -- the render-level view, and what
  a user rotating a phone to landscape would actually see.

The #259 E2E's 390 px resize step also asserts the other half of the fix
at the state level: the expansion is kept in expandedHashes, not cleared.

Review F4: the touch block's comment called ?hash= the Kpa-clawbot#866 deep link.
Kpa-clawbot#866 is #/packets/<hash>?obs=<id>, which does expand before the first
render; ?hash= only filters. The comment now says which case the block
covers and where the first-render-at-narrow-width case is covered.
Review F2 on #260: "the nodes list view adds no Escape listener" passed
for the wrong reason. The list view does add one -- nodesPanelEsc in
renderLeft() -- but only once the asynchronous loadNodes() resolves, and
the case asserted synchronously before that. It stated something untrue
about the page and could not fail.

Replaced with cases that await the render (the sandbox's api() resolves
immediately, so draining the microtask queue is enough) and then hold the
list view to one listener. Review F3 is the leak they expose:

- a region change re-runs loadNodes() -> renderLeft() inside one visit;
- list -> node -> list -> node -> list across five router cycles;
- destroy() must leave none behind.

Supporting sandbox changes: loadNodes()'s catch is silent, so console.error
is now recorded and every list-view case asserts it stayed empty -- a
missing stub would otherwise read as "no listener". history.replaceState
is recorded (nodesPanelEsc's observable effect), elements get a real
Set-backed classList and a dataset/value, and #nodesRight starts .empty
as the real markup renders it, so the handler's "only while a panel is
open" guard is the browser's guard.

Red on the parent commit: 2 listeners after two renders, 3 after the five
cycles, 1 left after destroy().
Review F3 on #260: nodesPanelEsc had the same leak class as the item-2
fix for _nodesEsc. It was a fresh named closure registered inside
renderLeft(), and renderLeft() runs on every load of the list -- a visit,
a region change, a filter change -- so the listeners stacked on document
and nothing ever removed them.

Same shape as the item-2 fix: _nodesPanelEsc is a module-level reference,
so a repeat addEventListener is a DOM no-op, and destroy() takes it off
alongside _nodesEsc. Behaviour is unchanged -- it still only acts while
#nodesRight is not .empty.

Pre-existing, but small and in the same file as item 2.
@dborup

dborup commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Rapport — CS-Minimax PR#260 runde 2 — head 35f184b

Review feedback addressed (commit 35f184b4, merge of origin/master c6b356de; the fixes are 8fd2be92, 09ba33b4 and f6abeef1).

Evidence tags: [T] test or run output, [A] assessment or inference, [K] checked in code, diff, git or CI.

1. F1 (blocking) — the 390 px "does not expand" assertions bite again

Agreed, and the diagnosis was exactly right: with groupIsExpandedInView(), a hash that is in expandedHashes renders at ≤ 600 px exactly like a collapsed row, so expanded + children could no longer fail on an expansion at that width. Fixed in two independent ways, because either one alone can be defeated. [K]

  • The state. public/packets.js exports _isExpanded(hash) on the existing _packetsTestAPI, next to _setExpanded. Both mobile E2Es' rowState() now carries inExpandedHashes, and every 390 px activation asserts it is false. If the hook is ever removed, the field is the string 'NO-HOOK', so its absence fails the assertion instead of silently passing it. [K]
  • The render. Both E2Es now resize back to 1400 px after the mobile activations and assert the group is still collapsed — aria-expanded="false", no expanded class, no child rows. That is the user-visible version of the regression you described: tap a group on a phone, rotate to landscape, and the group is open. [K]

Concretely:

File Steps changed
test-issue-254-affinity-toggle-mobile-aria-e2e.js tap, Enter and Space now go through one assertNotExpanded() helper (DOM and expandedHashes); new final step "back at 1400 px the group is still collapsed, so nothing expanded it" — 13 → 14 cases
test-issue-259-mobile-expanded-group-e2e.js the 390 px touch tap asserts inExpandedHashes === false; new step "390 px touch → 1400 px: the group is still collapsed"; the 1400 → 390 resize step now also asserts inExpandedHashes === true, i.e. the other half of the fix — the expansion is kept in state, not cleared — 9 → 10 cases

Your RM5b now dies. I rebuilt it as M1 (select-hash on a group-header row calls pktToggleGroup(value) and then pktSelectHash(value), so the sheet always opens and the group toggles unseen): [T]

  • test-issue-254-affinity-toggle-mobile-aria-e2e.js 11 passed, 3 failed — tap, Space, and the 1400 px round trip;
  • test-issue-259-mobile-expanded-group-e2e.js 8 passed, 2 failed — the touch tap and the round trip.

Verbatim from the run, the hook assertion and the render assertion each firing on their own:

✗ 390 px: a tap opens the detail sheet and does not expand the group: the tap did not expand
  the group: {"action":"select-hash","aria":null,"expanded":false,"children":0,"inExpandedHashes":true}
✗ back at 1400 px the group is still collapsed, so nothing expanded it: expected the desktop row,
  got {"action":"toggle-select","aria":"true","expanded":true,"children":3,"inExpandedHashes":true}

One detail worth recording: under M1 the Enter step still passes, because the preceding tap already toggled the group on and Enter toggles it back off. That is the same alternation your RM5 hit. It is why the round-trip step at the end of the block matters — it is the one assertion that is independent of how many times the mutant toggled. [T][A]

test-packets.js 146/146 and test-issue-189-group-caret-e2e.js 3/3 are unchanged under M1, as before. [T]

2. F2 — the nodes list-view case no longer claims something untrue

Agreed. "The nodes list view adds no Escape listener" asserted synchronously, before loadNodes() had resolved, so it could not fail. Rather than rename it, I took the option that tests the page: the list-view cases now await the asynchronous render and count what the page actually registers — exactly one listener. [K]

Supporting sandbox work, because loadNodes() swallows its own exceptions and a missing stub would otherwise read as "no listener": console.error is recorded and every list-view case asserts it stayed empty; history.replaceState is recorded (that is nodesPanelEsc's observable effect); elements get a real Set-backed classList plus dataset/value; AreaFilter.getAreaParam/getSelected and nodePassesGeoFilter are stubbed; and #nodesRight starts .empty as the real markup renders it, so the handler's "only while a detail panel is open" guard is the browser's guard. With that, renderLeft() runs to completion in the sandbox with zero logged errors. [T][K]

Mutant M2 — the list view registers no Escape listener at all (the addEventListener line dropped from renderLeft()): test-issue-259-nodes-esc-listener.js 7 passed, 4 failed (0 !== 1 on the registration case, the stacking cases, and "one Escape closes the panel exactly once" → []). The old case would have passed this mutant. [T]

3. F3 — nodesPanelEsc fixed in the same style

Taken along, as you suggested it could be. public/nodes.js:1679's named closure is now a module-level _nodesPanelEsc, so a repeat addEventListener is a DOM no-op, and destroy() removes it alongside _nodesEsc. Behaviour is unchanged — it still acts only while #nodesRight is not .empty. [K]

Worth noting the leak was a bit wider than per-visit: renderLeft() runs on every load of the list, so a region change or a filter change inside one visit added a listener too. The new cases cover that:

Case Before the fix After
the list view registers exactly one listener once its load resolves 1 1
a second render inside the same visit (a region change) does not add a second 2 1
list → node → list → node → list does not stack 3 1
one Escape closes the detail panel exactly once 1 1
destroy() removes the list view's listener 1 left 0

Your Chromium count of 3 after node A → list → node B → list reproduces in the sandbox as 3. [T]

I kept "one Escape closes the detail panel exactly once" even though it passes before the fix: with the real guard modelled, handler 1 adds .empty and handlers 2 and 3 bail, so the leak is latent rather than visible — your "harmless today" is right, and the listener count is the honest observable. It is there as a regression guard on the behaviour, not as proof of the leak. [A]

Mutant M3a — renderLeft() registers a fresh wrapper function (e) { _nodesPanelEsc(e); } (the pre-existing bug): 8 passed, 3 failed (2 after two renders, 3 after five cycles, 1 left after destroy()). Mutant M3b — the stable reference kept but destroy() no longer removes it: 10 passed, 1 failed. [T]

The file is now 11 cases, up from 7.

4. F4 — the Kpa-clawbot#866 deep link

Corrected. #866 is #/packets/<hash>?obs=<id>, not #/packets/<hash>/<obs>; ?hash= only filters and expands nothing. [K]

5. F5 and F6 — no change

  • F5 (test-packets.js:1326 is red on master only because the _setDisplayGrouped hook is missing). Accurate, and your own RM1 result is the reason the case earns its place: it is the only layer that kills a revert of _getRowCount alone. Left as it is. [A]
  • F6 (pre-existing, identical on master's packets.js: an ?obs= deep link at 1400 px renders the header expanded with 0 children until a later refresh; the desktop detail pane survives a flip to 390 px and must be closed with ×). Both are outside Follow-ups to #255: expanded group dead end on mobile, nodesEsc listener leak, two test gaps #259's four items and neither is a dead end. Not touched; recorded here so they are not lost. [A]

Your acknowledgement of M1b (the _invalidateRowCounts() call in the mode-flip handler is unobserved by any layer) still stands, and the call is still kept. [A]

Branch

codex/issue-259-followups-255, head 35f184b4. Three new commits, tests before fixes, then a merge commit for origin/master c6b356de. All four authored and committed by dborup <kontakt@meshview.dk>. No rebase, amend or force-push; only explicit git add. [K]

  1. 8fd2be92 test — F1's assertions and the _isExpanded hook, plus F4's comment.
  2. 09ba33b4 test — F2's rewrite and F3's cases (red on 8fd2be92).
  3. f6abeef1 fix — _nodesPanelEsc (green).
  4. 35f184b4 merge origin/master.

Clean merge, no conflicts. Master's only touch to public/nodes.js in that range is the advertIntervals argument on the two NodeAdverts.render(...) calls (#245), nowhere near this change. No closing keywords in the title, body or commits; closingIssuesReferences is empty. The PR stays a draft. [K]

Production diff for this round is 5 lines in public/packets.js (the test hook) and the _nodesPanelEsc move in public/nodes.js. Everything else is tests. [K]

Mutants [T]

Against the merged tree, one per finding, server restarted between the E2E ones, source restored from a scratchpad snapshot (not git checkout) and the tree verified clean after each.

Mutant Finding What test-packets.js nodes-esc unit #254 E2E #259 E2E #189 E2E Verdict
M1 F1 your RM5b: a select-hash group-header row calls pktToggleGroup(value) then pktSelectHash(value) 146/146 — 3 failed 2 failed 3/3 killed
M2 F2 renderLeft() registers no Escape listener at all — 4 failed — — — killed
M3a F3 renderLeft() registers a fresh wrapper closure per render (the pre-existing bug) — 3 failed — — — killed
M3b F3 stable reference kept, destroy() no longer removes it — 1 failed — — — killed

F4 is documentation only, so it has no mutant. F5 and F6 need no change. [A]

Suites (local, merged tree) [T]

A Go server built from this tree on a copy of e2e-fixture.db, prepared as in deploy.yml (freshen, the Kpa-clawbot#1486 and Kpa-clawbot#1791 seed SQL, corescope-migrate, then seeds 2073, 199 and 245), on port 13700, stopped by port afterwards.

Suite Result
sh test-all.sh 220 passed, 0 failed (220 files) — serial run, no flakes
node test-frontend-helpers.js 707 passed, 0 failed
node test-packets.js 146 passed, 0 failed
node test-issue-259-nodes-esc-listener.js 11 passed, 0 failed (was 7)
node test-issue-254-affinity-toggle-mobile-aria-e2e.js 14 passed, 0 failed (was 13)
node test-issue-259-mobile-expanded-group-e2e.js 10 passed, 0 failed (was 9)
node test-issue-189-group-caret-e2e.js 3 passed, 0 failed
node test-issue-1461-mobile-page-actions.js 6 passed, 0 failed (a unit file, no server needed — it is in test-all.sh, not deploy.yml)
sh scripts/check-xss-sinks.sh --diff origin/master exit 0, no output

CI [K][T]

Run 37363814293 on head 35f184b4, attempt 1, conclusion success. No reruns, and neither known flake fired — #267 (the backfill write-hold) and #271 did not fail, and nothing in the run was rerun.

Job Result Evidence from the log
✅ Go Build & Test success test-all.sh 220 passed, 0 failed (220 files); test-issue-259-nodes-esc-listener.js ran with all four new list-view cases green (the list view registers exactly one Escape listener once its load resolves, a second render inside the same visit does not add a second listener, list -> node -> list -> node -> list does not stack listeners, destroy() removes the list view's Escape listener); the PR-only XSS --diff origin/master preflight ran
✅ Playwright E2E Tests success test-issue-254-affinity-toggle-mobile-aria-e2e.js 14/14 (the new round-trip step included), test-issue-259-mobile-expanded-group-e2e.js 10/10, test-issue-189-group-caret-e2e.js 3/3
✅ Build & Publish Docker Image success —
📦 Release Artifacts skipped fork-guarded
🚀 Deploy Staging skipped fork-guarded
📝 Publish Badges & Summary skipped fork-guarded

One log line is worth pre-empting, because it looks alarming on a grep: [FAIL] STALE ALLOWLIST: 1 entry matched nothing this run in the Go job. That is test-a11y-axe-1668-selftest.js deliberately printing its own gate's failure text for a synthetic entry, and it ends PASS: a11y-axe-1668 selftest. Present on master too, unrelated to this PR. [K]

Not verified

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent1 PR#260 — head 35f184b

Dom: APPROVE with nits

Evidence tags: [T] test or run output, [A] assessment or inference, [K] checked in code, diff, git or CI.

This is round 2. The blocking finding from round 1 (F1) is fixed: the 390 px "does not expand" assertions bite again, at two independent layers. That includes RM5b, which survived round 1. All five of my packets mutants die, among them one that blinds the new test hook, so the render-level round trip carries it alone. F2 and F3 are fixed, and the fixes are proven by tests that are red on master and on the round-1 head. In a real browser, after A → list → B → list, there is exactly one nodes.js keydown listener (master: five). The two nits below do not block the merge.

Findings

# Severity Where Finding Evidence
N1 Nit (docs) public/packets.js:2420 F4 is fixed in the PR body and in the E2E comment. The production comment this PR added above groupIsExpandedInView() still names the Kpa-clawbot#866 deep link as #/packets/<hash>/<obs>. The real link is #/packets/<hash>?obs=<id> (packets.js:1232). One-line comment fix. [K]
N2 Nit (test) test-issue-259-nodes-esc-listener.js; public/nodes.js _nodesPanelEsc No test covers the handler's "only while a detail panel is open" guard. Mutant ND drops panel.classList.contains('empty') from the early return, and the nodes-esc unit still passes 11/11. In Chromium the same mutant has a visible effect: Escape on the list with no panel open rewrites #/nodes?search=zz to #/nodes. On the branch the URL is kept. The guard is pre-existing and was moved unchanged (the inverted early return is equivalent [K]), so this is optional. A case that presses Escape with #nodesRight.empty and asserts no replaceState would close the gap. [T][K]
I1 Info (pre-existing, out of scope) public/packets.js:2363 pktEsc in the packets page's init() has the same leak class as nodesEsc/nodesPanelEsc: a fresh named closure on document per init, never removed. In Chromium, after packets → (nodes → packets) ×3 there were 4 packets.js keydown listeners. Its effect, closeDetailPanel(), is idempotent, so it is harmless today. It is identical on master and not touched by this PR. It could be a follow-up. [T][K]

The author's M1b (the _invalidateRowCounts() call in the mode-flip handler is unobserved) and the earlier F5/F6 are still acknowledged and still not blocking. [A]

The review points

  1. F1 (was blocking): fixed. [T][K] rowState() in both mobile E2Es now reads expandedHashes through a new read-only _isExpanded(hash) hook on the existing _packetsTestAPI. A missing hook reads as 'NO-HOOK' and fails. Tap, Enter and Space go through assertNotExpanded(), which checks the DOM and the set. Both E2Es also resize back to 1400 px and require the toggle row with aria-expanded="false" and no children. My mutants against the merged tree:

    The 1400 → 390 step of the Follow-ups to #255: expanded group dead end on mobile, nodesEsc listener leak, two test gaps #259 E2E now also asserts inExpandedHashes === true. PE shows that this assertion bites: the state is kept, not cleared. As the author notes, under PA the Enter step passes on its own because the tap's toggle and Enter's toggle cancel out. The round trip and the hook on the tap and Space steps cover that.

  2. F2: fixed by testing the page, not by renaming. [T][K] The list-view cases await the asynchronous loadNodes() render and count what renderLeft() registers. The sandbox records console.error, and every list case asserts that it stayed empty, so a missing stub cannot read as "no listener". I did not rerun the author's "no listener at all" mutant (M2). Mutant NC below shows that a missing listener on a later render is caught (got 0).

  3. F3 (nodesPanelEsc): fixed, tested, mutants die. [T][K] _nodesPanelEsc is now module-level, renderLeft() adds the stable reference, and destroy() removes it next to _nodesEsc. The body is equivalent to the old closure.

    • The new test file on master's nodes.js has 7 failures; on the round-1 head 0ed1a3df's nodes.js it has 3 (the three new list-view cases); on the merged tree it is 11/11.
    • Chromium with CDP DOMDebugger.getEventListeners on document, keydown only: list → 1 nodes.js listener (line 599); node A → 1 (line 587, _nodesEsc); after A → list → B → list → 1; after leaving for packets → 0. Master's nodes.js in the same script: 5, then 4 left behind on packets.
    • Escape with the detail panel open closes it, writes #/nodes with one replaceState, and logs no page errors. This is the same as on master.
    • Mutants: NA (renderLeft() registers a per-render arrow wrapper) 3 failed; NB (destroy() no longer removes _nodesPanelEsc) 1 failed; NC (the listener is added only on the first render in the module's lifetime, so it is not re-added after destroy()) 2 failed. ND survives; see N2.
  4. F4: the PR text and the E2E comment are correct. [K] The PR body now names #/packets/<hash>?obs=<id>. It also says that the first-render-at-narrow-width case is covered by the test-packets.js unit cases, and that ?hash= only filters. The E2E touch-block comment says the same. The packets.js source comment was missed; see N1.

  5. No regression in fix(packets): collapsed groups show a right caret, expanded a down caret (#189) #248, fix(ui): Affinity Debug toggle and mobile group-row aria-expanded (#254) #255 or test: 13 red orphan unit tests + 4 orphan E2E files left after #187; collapsed packet groups show an up-caret #189. [T][K] The production diff for round 2 is the _isExpanded hook (read-only) and the _nodesPanelEsc move; nothing in the group-row rendering changed since round 1. On the merged tree:

Always-checks

Check Result
Every AC has a test that is red before and green after Items 1 and 2 as in round 1 [T]. F3's new list-view cases fail on master's and on 0ed1a3df's nodes.js and pass on the merged tree [T]. Items 3 and 4 are test-only and proven by mutants (RM4 in round 1; PA, PB, PC and PE now) [T].
Own mutants (9) PA–PE (packets), NA–ND (nodes): 8 killed, ND survives (N2) [T]
No behaviour change beyond the purpose Round 2's production diff is the read-only _isExpanded hook (on the _packetsTestAPI object that already carries _setExpanded) and _nodesPanelEsc, whose body is equivalent to the old closure [K]. The panel's Escape behaviour is unchanged in the browser [T].
cmd/server read-only, no new map[string]interface{} No .go file in the diff against origin/master f91339f2; 0 added occurrences [K]
No hardcoded colours No hex/rgb()/hsl() added outside comments (the only #… matches are issue numbers) [K]
scripts/check-xss-sinks.sh --diff origin/master exit 0, no output, on head and on the merged tree (scratch clone, origin/master = f91339f2) [T]
Fork guards 9 in deploy.yml, 1 in release-fast-path.yml, the same as master. deploy.yml gains only the #259 E2E line after the #254 line [K]
Closing keywords none in the title, body or commit messages; closingIssuesReferences is empty [K]
Commit author/committer all eight commits are dborup <kontakt@meshview.dk> for both [K]

Tests run (merged tree git merge-tree --write-tree origin/master 35f184b4… = 7da28b86 on origin/master f91339f2, clean merge) [T]

Suite Result
sh test-all.sh 221 passed, 0 failed (221 files) (master added one file since c6b356de)
node test-frontend-helpers.js 707 passed, 0 failed
node test-packets.js / nodes-esc / #254 unit / Kpa-clawbot#1461 unit 146/146, 11/11, 12/12, 6/6
cmd/server go test ./... ok on a serial rerun (370 s). The first run, in parallel with my E2E and mutant runs, had 1 failure: TestHandleAnalyticsSubpathDetailWithStore (coverage_test.go:2700, expected 200, got 503). The test requests /api/analytics/subpath-detail without waiting for the async subpath index, and the handler answers 503 until SubpathIndexReady() (routes.go:3021). It passed 20/20 in isolation (-count=20). The flake class is the same as the closed #227, in a different test. No Go code is in this PR, so it is master's code [T][K].
cmd/ingestor go test ./... ok (920 s)
E2E against a local Go server built from the merged tree on a copy of e2e-fixture.db, prepared as in deploy.yml (freshen, the Kpa-clawbot#1486/Kpa-clawbot#1791 seed SQL, corescope-migrate, seeds 2073, 199 and 245), restarted with a fresh fixture per mutant, stopped by port #259 10/10, #254 14/14, #189 3/3
Own Chromium scripts (CDP listener counts, Escape on the list, pktEsc) as reported above

CI on head 35f184b4: run 37363814293, attempt 1, success. Jobs:

Neither known flake fired: the Hash Stats sort (#256) passed, and the backfill write-hold (#267) did not fail. [K]

Not verified

  • CI on the merged tree against the current origin/master f91339f2; master moved after the branch's merge of c6b356de, and only my local runs cover that. [A]
  • The -race variant of the Go suites that CI runs. No Go code changes in this PR. [A]
  • Firefox, WebKit/Safari, a real phone, a real screen reader. [A]
  • Staging and production, by design. No API key was used.
  • No screenshots this round; every check was an attribute, state or listener assertion.

Head was 35f184b4dc8bd314b5a5a6a9ae53972a014b5946 on git ls-remote before and after the review. Nothing was pushed or changed on the PR.

#268 (issue #258, column widths) and this branch both appended a line to
test-all.sh's runner list. Kept both, in issue order next to #254:

  run test-issue-258-column-widths.js
  run test-issue-259-nodes-esc-listener.js

Everything else auto-merged and was verified to be the exact union of both
sides: .github/workflows/deploy.yml keeps the #258 and #259 E2E lines,
public/packets.js keeps #258's TableResponsive.unhidden() and #259's
groupIsExpandedInView(), and test-packets.js keeps both sets of cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dborup
dborup marked this pull request as ready for review October 6, 2026 02:37
@dborup
dborup merged commit edec425 into master Oct 6, 2026
11 of 12 checks passed
@dborup

dborup commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

Rapport — CS-Minimax PR#260 merge-konflikt — head bd1e9e5

origin/master 406f8481 (the merge of #268 / issue #258) merged into codex/issue-259-followups-255 as a merge commit bd1e9e5e. No rebase, no force-push — the push was a fast-forward 35f184b4..bd1e9e5e. [K]

Evidence tags: [T] test or run output, [A] assessment or inference, [K] checked in code, diff, git or CI.

1. The conflict — test-all.sh, one line against one line

git merge origin/master reported exactly one conflict, and the three files the task flagged as auto-merging did: [T]

Auto-merging .github/workflows/deploy.yml
Auto-merging public/packets.js
Auto-merging test-all.sh
CONFLICT (content): Merge conflict in test-all.sh
Auto-merging test-packets.js

Both sides had appended one run line immediately after run test-issue-254-affinity-debug-toggle.js, so the two additions landed on the same line and git could not order them. Both are kept, in issue order next to #254: [K]

run test-issue-254-affinity-debug-toggle.js
run test-issue-258-column-widths.js      # from #268 / master
run test-issue-259-nodes-esc-listener.js # from this branch
run test-perf-go-runtime.js

The result is the exact union of both sides, verified by diffing the merged file against each parent — each diff is additive only, and each adds exactly what the other parent contributed: [T]

merged test-all.sh vs added removed
branch 35f184b4 run test-app-api-bust-inflight-243.js, run test-issue-258-column-widths.js —
master 406f8481 run test-issue-259-nodes-esc-listener.js —

test-test-all.js independently confirms the resolution is well-formed: every registered file exists, no file is registered twice, every root test-*.js runs in test-all.sh or deploy.yml, 10/10. [T]

The merge commit carries no content of its own beyond that resolution. git show --cc bd1e9e5e — which lists only what differs from both parents — shows test-all.sh and nothing else: [T][K]

diff --cc test-all.sh
@@@ -58,7 -59,7 +59,8 @@@
  run test-issue-254-affinity-debug-toggle.js
+ run test-issue-258-column-widths.js
 +run test-issue-259-nodes-esc-listener.js
  run test-perf-go-runtime.js

2. The auto-merges — checked, both sets of changes intact

public/packets.js — the two PRs touch disjoint regions, so there was nothing to reconcile: #258 adds unhidden() and extends the window.TableResponsive export in the TableResponsive IIFE around lines 32-242; #259 adds groupIsExpandedInView(), the _invalidateRowCounts() call on the breakpoint crossing, its two call sites in buildGroupRowHtml() / _getRowCount(), and the _isExpanded / _setDisplayGrouped test hooks, from line ~2431 down. Diffing the merged file against each parent gives exactly the other parent's hunks, with no deletions: [K]

  • vs branch: +unhidden() (18 lines) and sweep: sweepDetached → sweep: sweepDetached, unhidden;
  • vs master: +groupIsExpandedInView(), +_invalidateRowCounts(), the two expandedHashes.has(p.hash) → groupIsExpandedInView(p.hash) call sites, +_isExpanded, +_setDisplayGrouped.

.github/workflows/deploy.yml — the union of both E2E lines, each still inside the Playwright step and each still on BASE_URL=http://localhost:13581: [K]

BASE_URL=... node test-issue-1122-details-row-clamp-e2e.js
BASE_URL=... node test-issue-258-column-widths-e2e.js     # from master
...
BASE_URL=... node test-issue-254-affinity-toggle-mobile-aria-e2e.js
BASE_URL=... node test-issue-259-mobile-expanded-group-e2e.js  # from this branch

test-packets.js — purely additive both ways: the merged file adds master's 39 lines over the branch and the branch's 95 lines over master, with 0 removed lines in either direction. [T]

3. Tests

Node suites, on the merge commit: [T]

Suite Result
sh test-all.sh 222 passed, 0 failed (222 files)
node test-frontend-helpers.js 707 passed, 0 failed
node test-packets.js 148 passed, 0 failed, 0 known bugs still failing
node test-issue-259-nodes-esc-listener.js 11 passed, 0 failed

test-packets.js is 148 here against the 146 reported in round 2 — the +2 are #258's cases arriving from master, not a change on this branch. [A][K]

E2E against a local corescope-server on port 13700 (cmd/server built from the merge commit, serving public/), with the fixture prepared the way deploy.yml does it — a scratch copy of test-fixtures/e2e-fixture.db, tools/freshen-fixture.sh, the inline Kpa-clawbot#1486/Kpa-clawbot#1791 seed, corescope-migrate, then the Kpa-clawbot#2073, #199 and #245 seed files. All four exit 0: [T]

E2E Result
test-issue-254-affinity-toggle-mobile-aria-e2e.js 14 passed, 0 failed
test-issue-259-mobile-expanded-group-e2e.js 10 passed, 0 failed
test-issue-258-column-widths-e2e.js 22 passed, 0 failed
test-issue-1122-details-row-clamp-e2e.js 18 passed, 0 failed

That is the answer to the question the merge actually raised — whether #258's column widths and #259's mobile/nodesEsc behaviour both still hold in the same tree. They do, from both directions: #258's "Details is >= 15% and expand <= 10% of the table" passes at 1200 px and 900 px across both fixture windows, and #259's "resize 1400 -> 390: no child row is left visible" / "resize 390 -> 1400: the children and the state come back" pass in light and dark, while #254's 390 px tap/Enter/Space cases and the 1400 px round trip still assert nothing expanded the group. [T]

4. CI

Run 37398668886. Attempt 2 is green on every job. Per job: [K]

Job Attempt 1 Attempt 2
✅ Go Build & Test failure — cmd/server hit the 20-minute go test timeout success (22m 45s)
🎭 Playwright E2E Tests skipped (needs: go-test) success
🏗️ Build & Publish Docker Image skipped (needs: e2e-test) success
📦 Release Artifacts skipped skipped
🚀 Deploy Staging skipped skipped — gated to push on refs/heads/master of Kpa-clawbot/CoreScope
📝 Publish Badges & Summary skipped skipped

The four E2Es this merge put at risk all pass in CI with exactly the counts I measured locally: #254 14 passed, 0 failed; #259 10 passed, 0 failed; #258 22 passed, 0 failed; Kpa-clawbot#1122 Details clamp passed 18 failed 0. [T]

The attempt-1 failure was the cmd/server time budget, not this merge

I re-ran it rather than changing anything, and the same commit passed. The evidence that it is a pre-existing master-side budget problem: [K][T][A]

  1. The merge adds no Go code at all. git diff 406f8481 bd1e9e5e -- '*.go' go.mod go.sum is empty — the merged tree differs from master only in public/nodes.js, public/packets.js, four test-*.js files, one test-all.sh line and one deploy.yml E2E line. Nothing in the timed-out package is reachable from any of them. [K]

  2. It was a wall-clock timeout, not a failing assertion or a deadlock. go test -timeout 20m -race -coverprofile on ./cmd/server:

    panic: test timed out after 20m0s
    	running tests:
    		TestTrackedBytesTracksTheHeap_113 (33s)
    FAIL	github.com/corescope/server	1200.263s
    

    One test was running, 33 s in; every other goroutine in the dump is an idle database/sql.(*DB).connectionOpener or healSchemaFlags select. The package had already printed coverage: 88.9% of statements — it ran out of total budget, nothing was stuck. [T]

  3. The same commit, same flags, 177 s apart across the two attempts — and the package was already at ~91 % of budget on the fix(ui): column widths ignore colspan rows and empty first renders (#258) #268 side before the merge:

    Run Commit cmd/server vs the 1200 s limit
    37329171525 — PR fix(ui): column widths ignore colspan rows and empty first renders (#258) #268's own green run da436cd6 1090.204 s (91 %)
    37323250705 — this branch, green 0ed1a3df 1061.690 s (88 %)
    37363814293 — this branch at 35f184b4, green 35f184b4 869.232 s (72 %)
    37398668886 attempt 1 — the merged tree bd1e9e5e timed out at 1200 s
    37398668886 attempt 2 — the merged tree bd1e9e5e 1023.028 s (85 %), coverage: 90.4%

    The run-to-run spread on identical or near-identical trees is ~220 s, larger than the ~110 s of headroom PR fix(ui): column widths ignore colspan rows and empty first renders (#258) #268 left. Merging the two sides puts fix(ui): column widths ignore colspan rows and empty first renders (#258) #268's new cmd/server tests (api_fallback_test.go, paths_confirm_deferred_test.go, pathlen_fast_test.go, plus the extended resolved_path_backfill_188_test.go / reach_rank_test.go / coverage_test.go) into a package that had little room for them — this branch at 35f184b4 did not have those tests, which is why its own runs were the fastest of the set. So the merge did not break the Go job, but it does leave it closer to the edge, and the next few points of growth in cmd/server will time out on master too. [A][T]

  4. Master has no green datapoint at 406f8481 to compare against: its own CI/CD run there (37398092159) was cancelled by the next push, and master has since moved on to 34f672c6. [K]

Per the task I made no other changes, so I did not touch -timeout or the slow tests. Raising the budget or splitting cmd/server is a master-side call and worth adding to the follow-up issue next to the round-2 nits. [A]

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.

2 participants