Skip to content

test(e2e): make the #180 packets URL reload step independent of fixture age (#271) - #280

Merged
dborup merged 3 commits into
masterfrom
codex/issue-271-packets-url-e2e-age
Oct 6, 2026
Merged

dborup merged 3 commits into
masterfrom
codex/issue-271-packets-url-e2e-age

Conversation

@dborup

@dborup dborup commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Relates to #271

Problem

The test-issue-180-packets-url-modal-e2e.js step "Clear Filters on
#/packets/<hash>?… closes the detail, writes #/packets, and a reload
shows the same list" compared row counts at the default 15-minute
window across two separate renders: Clear Filters' in-SPA list fetch, then
a full page reload's fetch. Both fetches compute since = now - 15min
from the browser's live clock (public/packets.js), so a fixture packet
can cross that boundary in the real-time gap between the two fetches. The
CI fixture ages while the Playwright job runs, so this surfaces as an
intermittent failure once the fixture is roughly 12-15 minutes old — same
class of problem #252 fixed for the Details advert-link test.

Fix (test only)

  • Keep the three assertions that are Clear Filters' own contract right
    after the click: bare #/packets URL, Clear button hidden, detail pane
    closed. These are exactly what a "Clear keeps ?timeWindow=" or "Clear
    keeps the detail open" mutant breaks.
  • For the list-identity check (the actual "reload shows the same list"
    claim), pin a wide 24h window (#fTimeWindow → 1440, the same choice
    test(packets): make the Details advert-link E2E independent of fixture age (#244) #252 made) before capturing "shown", then reload and recapture. Compare
    the set of packet hashes rendered (tr[data-hash], top-level rows
    only) rather than a count, so the comparison no longer depends on the
    fixture's age — a 24h margin against a few hundred ms of real time
    elapsed between fetches is not a boundary any of our test ages (0/13/120
    min) can reach.
  • Removed the now-unused listCount() helper.

No other test-issue-180-*, test-issue-1122-*, test-issue-189-*,
test-issue-254-*, or test-issue-1128-* file compares list state across
two renders at the unpinned default window — this pattern only existed in
this one step.

Evidence

Cause. Read public/packets.js: since = new Date(Date.now() - windowMin * 60000).toISOString() (computed fresh per request from the
browser's clock), DEFAULT_TIME_WINDOW = 15, and Clear Filters' handler
explicitly resets to that default. Demonstrated the resulting race
directly against a live server + aged fixture by polling
/api/packets?groupByHash=true&since=<now-15min> every 150ms: the grouped
packet count dropped 3 → 2 → 1 → 0 over ~2 minutes, each drop between two
polls 150ms apart, as individual packets aged out of the "last 15
minutes" window mid-run — the same mechanism the UI step hits, confirmed
without any UI involved.

Reproducing the failure through the actual Playwright step needs the
Clear-to-reload gap (~0.5-1s) to land on one of those narrow crossings;
~100 real runs against freshly-aged (13-14 min) fixtures in this session
didn't hit it live, consistent with it being an occasional CI flake (two
cited occurrences) rather than a per-run failure.

Fix validation, sh test-all.sh plus a local Go server against a
CI-prepared fixture (freshen, Kpa-clawbot#1486 seed row, corescope-migrate,
seed-2073, seed-199, seed-245), aged by shifting all now-derived
timestamps back the stated number of minutes:

Fixture age Runs Result
0 min 20 20/20
13 min 20 20/20
120 min 20 20/20

Mutants (both red with the fixed test, both reverted):

  • Commented out the savedTimeWindowMin = DEFAULT_TIME_WINDOW reset in
    Clear Filters' handler → URL after Clear: #/packets?timeWindow=60.
  • Commented out the closeDetailPanel() call in the same handler → detail
    pane assertion fails.

CI per job: pending — will report once CI finishes.

Rest: scripts/check-xss-sinks.sh --diff — no public/ changes (test
file only). Fork guards unchanged: 9 in deploy.yml, 1 in
release-fast-path.yml.

…re age (#271)

Relates to #271

The step compared row COUNTS at the default 15-min window across two
separate renders (Clear Filters' in-SPA list, then a full page reload).
Both fetches recompute "last 15 minutes" from the browser's live clock, so
a fixture packet can cross that boundary in the real-time gap between the
two fetches -- same class of problem #252 fixed for the Details test.

After the Clear-itself assertions (bare #/packets URL, no detail pane --
the ones a "Clear keeps ?timeWindow=/the detail" mutant breaks), pin a
wide 24h window for the list-identity check and compare the actual set of
packet hashes instead of a count, so the comparison cannot depend on how
old the fixture is.
@dborup

dborup commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Rapport — CS-MacBook PR#280 #271 — head add41a0

Status: fix implemented, validated locally, CI green.

Evidence legend: [T] = direct test/API execution this session, [A] = architecture/code-reading evidence, [K] = known/cited evidence from the issue's own CI-log quotes.

Cause

[A] public/packets.js: the "reload shows the same list" step compared row counts between two separate /api/packets fetches — Clear Filters' in-SPA fetch, then a full-page reload's fetch. Both compute since = new Date(Date.now() - windowMin * 60000).toISOString() (line ~1561) from the browser's live clock, with DEFAULT_TIME_WINDOW = 15 (line 821) and Clear Filters' handler explicitly resetting to that default (lines 2086-2090). Any fixture packet whose age crosses the 15-minute mark in the real-time gap between the two fetches makes the counts disagree.

[T] Reproduced the exact mechanism directly against a live server (no UI involved): prepared a fixture aged ~13 minutes (CI-style: freshen, Kpa-clawbot#1486 seed row, corescope-migrate, seed-2073/199/245, then shift all now-derived timestamps back 13 min), and polled GET /api/packets?groupByHash=true&since=<now-15min> every 150ms. The grouped count dropped 3 → 2 → 1 → 0 over about 2 minutes, with each drop occurring between two polls only 150ms apart — i.e., two "last 15 minutes" fetches 150ms apart can legitimately disagree, which is exactly what made "Clear showed N / reload showed N-1" intermittent in CI.

[K] Matches the issue's own cited failures (master ae126374: "reload shows 5, Clear showed 6"; PR #257 02b99c45: "reload shows 3, Clear showed 5") — same direction (reload ≤ Clear, never more), consistent with packets only ever aging out of a shrinking window, never back in.

I also ran ~100 real Playwright executions of the original (unfixed) step against freshly 13-14-minute-aged fixtures, back-to-back, without catching a live UI-level failure — consistent with this being a rare CI flake (needs the ~0.5-1s Clear→reload gap to land on one of a handful of narrow per-minute crossings) rather than a per-run failure. The direct-API evidence above is what establishes the mechanism with certainty; the Playwright attempts are noted for completeness, not as the primary evidence.

Fix

Test-only change to test-issue-180-packets-url-modal-e2e.js, item 2 ("Clear Filters on a detail URL"):

  • Kept the three assertions that are Clear Filters' own contract, run immediately after the click: bare #/packets URL, Clear button hidden, detail pane closed. These are untouched and are exactly what the mutant below breaks.
  • For the "reload shows the same list" claim specifically, pin a wide 24h window (#fTimeWindow → 1440, same choice test(packets): make the Details advert-link E2E independent of fixture age (#244) #252 made for the Details test) before capturing the list, then reload and recapture — comparing the set of rendered packet hashes (#pktBody > tr:not(.group-child) → dataset.hash, sorted) instead of a row count. A 24h margin against the sub-second gap between two fetches is not a boundary any realistic fixture age reaches.
  • Removed the now-unused listCount() helper.

Checked all other test-issue-180-*, test-issue-1122-* (details-row-clamp, packets-filter-ux), test-issue-189-*, test-issue-254-*, and test-issue-1128-* files for the same pattern (row counts/state compared across two renders at the unpinned default window): none exist — test-issue-1122-details-row-clamp-e2e.js already pins a wide window per #252, and the others either take a single snapshot or use an explicit hash=+timeWindow=0 deep link. No further files needed the same fix.

No public/ changes — verified with scripts/check-xss-sinks.sh --diff (no JS/HTML diff to scan).

Fixture age × runs (fixed test, full file incl. the item-2 step)

[T] Local Go server (corescope-server) against a CI-prepared fixture, aged by shifting all now-derived timestamps back the stated number of minutes right after the standard freshen/migrate/seed steps:

Fixture age Runs Result
0 min 20 20/20
13 min 20 20/20
120 min 20 20/20

Mutant

[T] Both reverted after confirming red, against the fixed test:

  • Commented out savedTimeWindowMin = DEFAULT_TIME_WINDOW; in Clear Filters' handler → ✗ ... URL after Clear: #/packets?timeWindow=60 (1 failed).
  • Commented out the closeDetailPanel(); call in the same handler → detail-pane assertion fails (1 failed).

git diff public/packets.js is empty — both mutants fully reverted, no product-code changes shipped.

CI per job (run 37370892842, head add41a0)

  • ✅ Go Build & Test — pass (20m35s)
  • ✅ 🎭 Playwright E2E Tests — pass (27m22s) — includes the fixed test-issue-180-packets-url-modal-e2e.js
  • ✅ 🏗️ Build & Publish Docker Image — pass (45s)
  • ⏭️ 📦 Release Artifacts, 📝 Publish Badges & Summary, 🚀 Deploy Staging — skipping (expected; this is a PR, not a push to master)

Rest

  • [A] Fork guards unchanged: 9 in .github/workflows/deploy.yml, 1 in .github/workflows/release-fast-path.yml (grep-verified before and after).
  • [T] sh test-all.sh: 219/219 unit test files passed, 0 failed.
  • [T] Also ran the adjacent packets E2E suite (test-issue-147-packets-url-obs-e2e.js, test-issue-96-hide-control-e2e.js, test-issue-1692-packets-init-parallel-e2e.js) against the fixed test's server — all green, no regressions.
  • PR is a draft; no merge, approve, ready, or issue-close action taken, per the brief.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent1 PR#280 — head add41a0

Dom: REQUEST CHANGES

The fix removes the clock race (verified below), but the step no longer reloads the URL Clear wrote. A real regression class that master's step caught now passes. The fix is small, and I validated a version of it (finding 1).

Evidence legend: [T] = executed in this review, [A] = code reading, [K] = cited from the issue, the PR or CI.

Findings

# Severity Where Finding
1 Medium test-issue-180-packets-url-modal-e2e.js:120-138 The step now reloads #/packets?timeWindow=1440, not the bare #/packets that Clear wrote. The removed Clear button back after reload check had nothing equivalent left. A regression where reloading Clear's URL does not give Clear's state now passes. This is mutant M5 below: master's step goes red, this PR's step stays green.
2 Low (nit) same file, lines 126-129 Because the table is virtualized, hashSet() sees only the rendered rows: 63 tr, including 2 spacer rows whose dataset.hash is undefined, out of 499 packets in the 24h window [T]. The comparison is deterministic, but the comment ("the actual set of packet hashes") overstates it. Consider filtering out rows without a hash and also comparing the #pktLeft .count total, which is clock-independent at 24h.
3 Nit lines 104-106 The comment says the three assertions include "the saved window back at default". The three assertions are the URL, Clear hidden and the detail closed. The window reset is only implied by Clear being hidden.
4 Info PR evidence The PR did not show the UI step red; it used API polling only. I produced a deterministic red with the unmodified master step (below), so acceptance item 1 now has evidence.

Suggested fix for finding 1 (validated in scratch, not pushed)

Right after the three Clear assertions, reload what Clear wrote and compare state rather than rows. Then keep the PR's 24h hash comparison as it is:

// Reload exactly what Clear wrote and compare state, not rows.
const twAfterClear = await page.evaluate(() => document.getElementById('fTimeWindow').value);
await Promise.all([listLoaded(), page.reload({ waitUntil: 'load' })]);
await page.waitForTimeout(500);
h = await page.evaluate(() => location.hash);
assert(h === '#/packets', 'URL after reloading the Clear state: ' + h);
assert(!(await clearShown(page)), 'Clear button back after reload');
const twAfterReload = await page.evaluate(() => document.getElementById('fTimeWindow').value);
assert(twAfterReload === twAfterClear, 'time window after reload ' + twAfterReload + ', after Clear ' + twAfterClear);
assert(!(await detailPaneOpen(page)), 'detail pane open after reloading the Clear state');

Results for this variant [T]:

  • It catches M1, M2 and M5.
  • It passes 3/3 under the deterministic 15-min crossing. In those runs, Clear's fetch landed before the crossing and the bare reload's fetch after it.
  • It passes 10/10 at fixture ages 0, 13 and 120 min.

Answers to the review points

  1. Cause, red before and green after: confirmed. [T] The server keeps a grouped packet while FirstSeen >= floor(since), string compare in filterPackets, store.go. since is computed from the browser clock on each fetch (public/packets.js:1579) [A].
    • I aged the CI-prepared fixture so that the newest packet leaves the 15-min window at a known wall-clock second, then launched the step about 1 s earlier. Clear's /api/packets fetch then lands just before the crossing and the reload's fetch about 0.6 s later, just after it.
    • The unmodified master file failed 3/3, plus 1/1 with the item-2 extract: ✗ … reload shows 0 packets, Clear showed 1. Item 2 was the only failing step.
    • This PR's step passed 5/5 under the same conditions. The request log shows the 15-min crossing falling between Clear's fetch and the 24h fetches.
    • Separately, the master step also failed once on its own: reload shows 53 packets, Clear showed 54 at fixture age ≈3 min. It passed 5/5 on rerun, so this is the flaky: #180 packets URL E2E 'reload shows the same list' depends on fixture age (default 15-min window) #271 flake itself [T].
  2. Clock independence: confirmed. [T] The item-2 step passed 20/20 at fixture ages 0, 13 and 120 min. The full file passed 12/12 once at each age.
    • Margin [T]: at age 120 min, the oldest packet inside the 24h window is 4.1 h old and the newest outside it is about 27 h old. A packet would need roughly 20 h of fixture age to reach the 24h edge.
    • The step still proves that Clear writes #/packets, hides Clear and closes the detail.
    • "A reload shows the same list" is now proven only for a pinned 24h URL, not for Clear's own URL. See finding 1.
  3. Mutant where Clear keeps ?timeWindow= or the detail: red. [T] Results in the mutant table below.
  4. Other test-issue-180-* steps: none affected.

Tests and mutants

These ran on the merged tree: git merge-tree --write-tree origin/master add41a09… against origin/master 406f8481, giving tree 1a3f55d7. The local Go server was built from that tree. The fixture was prepared as in CI: freshen, the Kpa-clawbot#1486/Kpa-clawbot#1791 seed SQL from deploy.yml, corescope-migrate, then seeds 2073, 199 and 245.

Suite Result
cmd/server go test ./... ok 357.9 s, -timeout 20m as in CI [T]. A first run at the default 10 m timed out (in Test1690_BackgroundLoadHonesty) while E2E runs and test-all.sh were loading the box; the clean rerun passed.
cmd/ingestor go test ./... ok 577.8 s, -timeout 20m [T]. Same note: the first run at the default 10 m timed out under the same load.
sh test-all.sh 221/221 files passed on a quiet box [T]. A first run under load had 2 timing failures in test-channels-client-state-152.js (F1 and R4-3 S2). That file passed 70/70 ×5 standalone on both master and the merged tree; it is unrelated to this test-only diff.
node test-frontend-helpers.js 707 passed, 0 failed [T]
test-issue-180-packets-url-modal-e2e.js (full file) 12/12 at ages 0, 13 and 120 [T]
Item-2 step, ×20 per age 20/20 at ages 0, 13 and 120 [T]
scripts/check-xss-sinks.sh --diff origin/master no public/** changes to scan, exit 0 [T]

Mutants in public/packets.js (served from a mutated copy; each row ran the PR's step and master's step) [T]:

Mutant PR step master step
M1: Clear keeps the window (savedTimeWindowMin = DEFAULT_TIME_WINDOW removed) ✗ URL after Clear: #/packets?timeWindow=60 ✗ same
M2: Clear keeps the detail (closeDetailPanel() removed) ✗ detail pane still open after Clear ✗ same
M3 (own): Clear keeps the subpath (updatePacketsUrl({ obs: null })) ✗ URL after Clear: #/packetsundefined ✗ same
M4 (own): load ignores ?timeWindow= in the URL (line 1243) ✗ URL after reload: #/packets ✓ survives (verified 5/5)
M5 (own): DEFAULT_TIME_WINDOW = 30, so Clear's default no longer matches the load default, which is hardcoded 15 at line 828 ✓ survives ✗ URL after reload: #/packets?timeWindow=15

M4 shows the new 24h reload is not vacuous. M5 is finding 1.

Other checks

Not verified

  • I did not reproduce a CI-runner failure of the PR's step. The clock-independence claim rests on the local runs above and on the 24h margin.
  • The fixture ages were simulated by shifting now-derived first_seen and observations.timestamp values after the CI-style preparation. I did not let a fixture age in real time.
  • I did not run the full Playwright E2E suite locally, only the fix(packets): leaving with the SlideOver open rewrites the new page's URL; Clear Filters on a detail URL re-filters on reload #180 file. Other packets E2Es are unaffected because the diff is confined to this file.
  • I did not check the browser visually; the change is test-only.

dborup added 2 commits October 6, 2026 08:06
…acer rows (#271)

Round-2 review findings on PR #280:

1. The step pinned 24h before its only reload, so it reloaded
   `#/packets?timeWindow=1440` instead of the bare `#/packets` Clear
   wrote, and nothing replaced the removed "Clear button back after
   reload" check. Reload what Clear wrote first and compare state —
   URL, Clear button, the saved window value, the detail pane, the hash
   input — all of which are clock-independent; then do the pinned-window
   list comparison as before.

2. The table is virtualised, so `#pktBody > tr:not(.group-child)`
   also matches the two #vscroll-* spacer rows, whose `dataset.hash` is
   undefined. Drop rows without a hash, assert at least one real row is
   rendered, and also compare the "Latest Packets (N)" total, which is
   the whole filtered set and clock-independent at 24h.

3. Corrected the comment that claimed the three post-Clear assertions
   include the saved window going back to the default; they are the URL,
   the Clear button and the detail pane.

Test-only; no product change.
@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Rapport — CS-Macmini PR#280 runde 2 — head a032471

Review feedback addressed (commit a032471a). origin/master 72cc29cf merged in as 6abfed1e. Draft, test-only: git diff --stat origin/master...HEAD is one file, test-issue-180-packets-url-modal-e2e.js, +52/−21.

Evidence legend: [T] = executed this session, [A] = code reading, [K] = cited from the issue/PR/CI.

Findings

  1. Medium — the step no longer reloaded the URL Clear wrote (test-issue-180-packets-url-modal-e2e.js:120-138). Fixed.
    The 24h pin happened before the step's only reload, so the reload exercised #/packets?timeWindow=1440 and nothing replaced the removed Clear button back after reload check. The step now reloads the bare #/packets Clear wrote, immediately after the three Clear assertions, and compares state — URL, Clear button hidden, #fTimeWindow value identical to the value right after Clear, detail pane closed, #fHash empty — all clock-independent. The pinned-window list comparison then runs as before. Implemented as the review suggested, with #fHash moved up to the bare reload (it is the fix(packets): keep ?obs=/?viewPath= on packet detail URLs (#147) #167 "subpath sets the hash filter again" assertion, which belongs on Clear's own URL).
    [T] Mutant M5 (DEFAULT_TIME_WINDOW = 30, so Clear's default no longer matches the 15 a cold load restores at public/packets.js:828), all three step versions against the same mutated packets.js:

    Step Result
    origin/master ✗ URL after reload: #/packets?timeWindow=15
    PR head add41a09 ✓ 12/12 — the regression this finding reports
    head a032471a ✗ URL after reloading the Clear state: #/packets?timeWindow=15
  2. Low (nit) — virtualised rows and the overstated comment (lines 126-129). Fixed.
    #pktBody > tr:not(.group-child) also matches the two virtual-scroll spacer rows, whose dataset.hash is undefined. The step now drops rows without a hash (.filter(Boolean)), asserts at least one real row is rendered, and additionally compares the #pktLeft .count total — which is displayPackets.length, the whole filtered set (public/packets.js:3130 [A]), so it is virtualisation-independent and, at 24h, clock-independent. The comment now says total is the full filtered count and hashes is what is rendered, instead of claiming "the actual set of packet hashes".
    [T] Measured at 1400×900, #/packets?timeWindow=1440: count (499), 62 top-level tr, 60 with a data-hash, 2 without (#vscroll-top, #vscroll-bottom) — matching the review's observation.
    [T] Mutant M6 (renderVisibleRows: const { startIdx, endIdx } = { startIdx: 0, endIdx: 0 } — the table renders no data rows at all, count still (499)):

    Step Result
    origin/master ✓ survives (compares the (499) total only)
    PR head add41a09 ✓ survives — shown was [undefined, undefined], so shown.length > 0 passed on an empty table
    head a032471a ✗ no rendered rows with a hash to compare at the pinned 24h window

    M6 is the concrete cost of the vacuity: both the old and the PR-head comparison passed with zero rows rendered.

  3. Nit — comment at lines 104-106. Fixed. It now names exactly the three assertions (URL, Clear button, detail pane) and says the window reset is only implied by Clear being hidden, with the actual window check attributed to the reload added for finding 1.

  4. Info — the PR's evidence did not show the UI step red. Addressed with a deterministic repro.
    [T] Built a crossing harness: after the CI-style fixture prep, the 60 newest transmissions.first_seen / observations.timestamp are rewritten so exactly one packet leaves the 15-min window per second starting at a computed T0, with every other row pushed to −2 h so it cannot move. The item-2 step then runs back to back across that stream:

    Step Result across the same crossing stream
    origin/master item 2 14 failed / 23 runs — reload shows 52 packets, Clear showed 53, … 50 / 51, … 48 / 49, …
    head a032471a item 2 15 passed / 15 runs, 0 failed

    The failure messages are the same shape as the two CI occurrences the issue quotes [K], and always reload ≤ Clear. Acceptance item 1 (red before, green after) now has a repeatable UI-level demonstration rather than API polling only.

Regression mutants kept red

[T] Same mutated public/packets.js served to the step, each reverted afterwards; git diff public/ is empty.

Mutant head a032471a
M1: Clear keeps the window (savedTimeWindowMin = DEFAULT_TIME_WINDOW removed) ✗ URL after Clear: #/packets?timeWindow=60
M2: Clear keeps the detail (closeDetailPanel() removed) ✗ detail pane still open after Clear
M4: cold load ignores ?timeWindow= (public/packets.js:1242) ✗ URL after reloading the pinned window: #/packets
M5: finding 1 ✗ (table above)
M6: finding 2 ✗ (table above)

M4 still shows the pinned 24h reload is not vacuous, and M5/M6 are the two the PR head let through.

Tests

[T] Local Go server built from this tree (cmd/server, cmd/migrate) on port 13810, against a CI-prepared fixture: tools/freshen-fixture.sh, the Kpa-clawbot#1486 and Kpa-clawbot#1791 seed SQL from deploy.yml, corescope-migrate, then seeds 2073, 199 and 245. Fixture ages simulated by shifting transmissions.first_seen, observations.timestamp and nodes.last_seen back the stated number of minutes after that prep.

Suite Result
item-2 step ×20 at fixture age 0 min 20/20
item-2 step ×20 at fixture age 13 min 20/20
item-2 step ×20 at fixture age 120 min 20/20
item-2 step across the 1-crossing-per-second stream 15/15
full test-issue-180-packets-url-modal-e2e.js at ages 0 / 120 12/12, ×3 each
full file at age 13 ×3: one run hit ✗ … (#packetPathClose) closes it: focus did not move into the modal (item 4); ×6 rerun 12/12. Item 4 is untouched by this diff — it contains no list comparison and no clock dependency
sh test-all.sh 222/222 files passed, 0 failed
node test-frontend-helpers.js 709 passed, 0 failed
cmd/server go test -timeout 20m ./... ok 44.6 s on a quiet box. A first run while the Playwright runs were still loading the machine failed TestHandleNodePaths_FallbackUniquePrefix_1352 and TestHandleNodePaths_FallbackUnresolvableHop_1352 with code=503 body={"error":"index loading","retryAfter":5}; the targeted rerun passed in 0.9 s and the clean full rerun passed. Load-sensitive and unrelated — this diff touches no Go code
cmd/ingestor go test -timeout 20m ./... ok 107.1 s
test-issue-147-packets-url-obs-e2e.js 12/12
test-issue-96-hide-control-e2e.js 10/10
test-issue-1122-details-row-clamp-e2e.js 18 passed, 0 failed
test-issue-1122-packets-filter-ux-e2e.js 6 passed, 0 failed
scripts/check-xss-sinks.sh --diff origin/master no public/**/*.{js,html} changes to scan, exit 0

CI per job

[T] Run 37424549407, head a032471a — run conclusion success.

  • ✅ Go Build & Test — pass
  • ✅ 🎭 Playwright E2E Tests — pass. test-issue-180-packets-url-modal-e2e.js in the log: 12 passed, 0 failed, with the item-2 step green
  • ✅ 🏗️ Build & Publish Docker Image — pass
  • ⏭️ 📦 Release Artifacts, 🚀 Deploy Staging, 📝 Publish Badges & Summary — skipped, as expected for a PR

No job needed a rerun; neither known flake (#271, #301) appeared.

Rest

  • [A] Fork guards unchanged: 9 in .github/workflows/deploy.yml, 1 in .github/workflows/release-fast-path.yml.
  • [T] Commit author dborup <kontakt@meshview.dk>; no rebase, amend or force-push; only test-issue-180-packets-url-modal-e2e.js staged.
  • No closing keywords; the PR stays a draft. No merge, ready or issue-close action taken.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent1 PR#280 — head a032471

Dom: APPROVE with nits

Round 2 fixes the round-1 Medium. The step now reloads the bare #/packets that Clear wrote and compares state. M5 is red on head and on master and green only on round 1. With a fixture aging in real time, master's step failed 3 of 50 runs and head's step passed 50 of 50. The four nits below cover pre-existing gaps and wording. None of them blocks the merge.

Evidence legend: [T] = executed in this review, [A] = code reading, [K] = cited from the issue, the PR or CI.

Findings

# Severity Where Finding
1 Low (nit, pre-existing) test-issue-180-packets-url-modal-e2e.js, item 2 #fHash is asserted empty only after the reload. After the reload it is rebuilt from filters.hash. Own mutant M8 makes Clear keep the text in the #fHash input (public/packets.js:2083 removed), and it survives on master, round 1 and head [T]. This is not a regression, since master asserts in the same place. One more assert(fHash === '') right after the three Clear assertions would catch M8. Optional.
2 Info (pre-existing harness limit) newPage() addInitScript The init script removes meshcore-time-window before every navigation, reloads included. As a result, no reload in this file can catch a Clear that leaves a stale saved window in localStorage. Own mutant M7 (public/packets.js:2108 removed) survives on all three versions [T]. Out of scope for #271, but worth knowing.
3 Nit (PR text) PR description "surfaces as an intermittent failure once the fixture is roughly 12-15 minutes old" is too narrow. Freshen spreads the fixture's packets over time, so the 15-min edge crosses a packet at almost any age while packets remain in the window. With real-time aging, master's step failed at fixture ages 79 s, 101 s and 168 s [T] (table below). The fix does not depend on this, but the description of the cause should not either.
4 Nit comment above the 24h pin "24h; the widest option": on desktop, "All time" (0) is also offered (public/packets.js:1761) [A]. 24h is the widest bounded option. "Widest bounded option" or "widest selectable window, as #252" would be accurate.

Answers to the review points

  1. Round-1 finding 1 (Medium): fixed. [A] Right after the three Clear assertions, the step reloads the bare #/packets and asserts the following:

    • the URL;
    • that Clear is hidden;
    • #fTimeWindow is identical to the value right after Clear;
    • the detail pane is closed;
    • #fHash is empty.

    Only then does it pin 24h. [T] Mutants were served from a mutated copy of public/packets.js. Each row ran master's step, round 1's step (add41a09) and head's step against the same server:

    Mutant master round 1 add41a09 head a032471a
    M5: DEFAULT_TIME_WINDOW = 30 (:839) ✗ URL after reload: #/packets?timeWindow=15 ✓ survives ✗ URL after reloading the Clear state: #/packets?timeWindow=15
    M9 (own): a cold load shows 30 in #fTimeWindow while applying the saved window (:2132) ✓ survives ✓ survives ✗ time window after reload 30, after Clear 15
    M10 (own): a cold load of bare #/packets reopens the last-viewed packet detail (remembered in sessionStorage) ✗ URL after reload: #/packets/725cc… ✗ … #/packets/725cc…?timeWindow=1440 ✗ URL after reloading the Clear state: #/packets/725cc…
    M8 (own): Clear keeps the #fHash input text (:2083) ✓ survives ✓ survives ✓ survives (finding 1)
    M7 (own): Clear keeps localStorage meshcore-time-window (:2108) ✓ survives ✓ survives ✓ survives (finding 2)
    • M5 is red on head and on master, as required.
    • M9 shows that the new #fTimeWindow comparison is load-bearing, because only head catches it.
    • M10 is the "detail pane comes back after reload" class, and all three versions catch it.
  2. Clock independence (flaky: #180 packets URL E2E 'reload shows the same list' depends on fixture age (default 15-min window) #271's purpose): confirmed.

    • Real-time aging [T]: I freshened one fixture as in CI, started a server and then ran the full file in a loop for 18 minutes, alternating merged-tree head and master. The fixture aged in real time from 25 s to 1102 s (0.4 to 18.4 min). This covers both a fresh freshen and one that was several minutes old, including the 12-15 min window.

      Step Runs Failed
      head (merged tree) 50 0
      master 50 3: reload shows 46 packets, Clear showed 47 (age 79 s), … 45 / 46 (101 s), … 37 / 38 (168 s)

      This is red before and green after, under real aging rather than simulated aging. Every master failure has reload = Clear − 1, the same shape as the two CI occurrences the issue quotes [K].

    • Simulated 120-min age [T]: I prepared the fixture as in CI and then shifted transmissions.first_seen, observations.timestamp and nodes.last_seen back 120 min. Head passed 10/10 and master 10/10. Master passes here only because nothing is left in the 15-min window.

    • CI [K/T]: on run 37424549407, freshen ran at 06:59:36Z and the fix(packets): leaving with the SlideOver open rewrites the new page's URL; Clear Filters on a detail URL re-filters on reload #180 step at 07:12:53Z. The fixture was ~13 min old in CI, inside the danger zone, and the step was green.

    • The remaining clock-dependent parts are clock-independent by construction [A]. The bare reload's 15-min fetch is awaited but its rows are never compared. The list comparison runs at 24h. At 24h, (499) is the whole fixture, and a packet would need many hours of extra age to reach the edge.

  3. Round-1 finding 2 (nit): fixed. [A] listState() drops rows without data-hash (.filter(Boolean)). It asserts hashes.length > 0 and also compares the #pktLeft .count total. [T] At 1400×900 and #/packets?timeWindow=1440, the table had 63 top-level tr, 61 with a hash and 2 without (vscroll-top, vscroll-bottom), against a count of (499). The spacers are filtered out, and the comment now says the hashes are the rendered rows, not the whole set. Round-1 finding 3 (comment wording) is also fixed.

  4. Scope: OK. [T] git diff --stat origin/master...a032471a shows a single file, test-issue-180-packets-url-modal-e2e.js (+59/−13). Compared with master's step, every assertion is kept:

    • URL after Clear;
    • Clear hidden;
    • detail closed;
    • URL after reload;
    • Clear button back after reload;
    • detail open after reload;
    • #fHash after reload.

    Only reload count === Clear count at the default window changed: it is replaced by the state comparison plus the pinned 24h comparison. Items 3-6 and the other fix(packets): keep ?obs=/?viewPath= on packet detail URLs (#147) #167/fix(packets): leaving with the SlideOver open rewrites the new page's URL; Clear Filters on a detail URL re-filters on reload #180 steps are untouched. The removed listCount() has no remaining callers [T].

  5. CI: green per job. [T] Run 37424549407 on head a032471a, conclusion success:

Tests

The merged tree is git merge-tree --write-tree origin/master a032471a…. The Go server and corescope-migrate were built from it. The fixture was prepared as in CI:

Servers were stopped by port.

Suite Tree Result
cmd/server go test -timeout 20m ./... merged with 22589f05 ok 590.7 s [T]
cmd/ingestor go test -timeout 20m ./... merged with 22589f05 ok 824.6 s [T]
sh test-all.sh merged with 22589f05 222/222 [T]
node test-frontend-helpers.js merged with 22589f05 709 passed, 0 failed [T]
test-issue-180-packets-url-modal-e2e.js, real-time aging 0.4-18.4 min merged with 22589f05 50/50 (master step 47/50) [T]
same file, simulated age 120 min merged with 22589f05 10/10 [T]
mutants M5, M7, M8, M9, M10 × {master, round 1, head} merged with 22589f05 table above [T]
scripts/check-xss-sinks.sh --diff origin/master head no public/**/*.{js,html} changes to scan, exit 0 [T]
origin/master moved to 298609e0 during the review (36 files, including public/packet-path-map.js). Re-merge 24ebbd24 is clean. On it, test-issue-180-packets-url-modal-e2e.js passed ×5, test-all.sh 222/222 and test-frontend-helpers.js 709/0 merged with 298609e0 green [T]

Other checks

  • cmd/server stays read-only. There is no new map[string]interface{} and no hardcoded color, because the diff is one JS test file [T].
  • Fork guards [T]: 9 in deploy.yml and 1 in release-fast-path.yml, on master, on head and on the merged tree.
  • Closing keywords [T]: none in the title, body or commits. The PR says "Relates to flaky: #180 packets URL E2E 'reload shows the same list' depends on fixture age (default 15-min window) #271".
  • Commit author [T]: all three commits are dborup <kontakt@meshview.dk>.
  • Head was a032471a before and after the review (git ls-remote). The PR is still a draft.

Not verified

@dborup
dborup marked this pull request as ready for review October 6, 2026 09:10
@dborup
dborup merged commit 03f9a9d into master Oct 6, 2026
6 checks passed
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.

3 participants