Repository navigation
tests: deflake pre-existing flaky Go and E2E tests - #571
Conversation
TestRepeaterRelayInfoMap_ConcurrentIngestAndEviction builds its oracle with collectRelayEntriesLocked(key) and no metadata snapshot, which falls back to the node cache. That cache is cold until the first bulk reader warms it, so the round-0 oracle credits no relay while the bulk reader (which loads the cache) credits 100 per relay. A reader that finishes before round 1 then "matches no store state" — the flake (4/150 linux/amd64, 66/150 darwin/arm64). This assertion makes the mismatch deterministic: red 30/30. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The oracle called collectRelayEntriesLocked(key) without a metadata snapshot, so it read the node cache, which is nil until the first bulk reader warms it. Round 0 was therefore published without relay identities while every bulk read resolved them. Snapshot the metadata once, the way GetRepeaterRelayInfo does, so every round describes the evidence the bulk reader sees. Test-only: the product bulk/single paths already agree. 150/150 green under 2x-nproc CPU load, with and without -race. The lock mutant (bulk key traversal after RUnlock) is still killed: DATA RACE under -race, and mismatches/panic without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load() builds the path-hop index in a background goroutine and
/api/nodes/{pk}/paths answers 503 "index loading" until it is ready (Kpa-clawbot#1008,
by design). Several /paths tests queried right after Load(), so under CPU
load they raced the build: SortByRecency_1145 failed 50/100 and
PrefixCollision_1352_FallbackBranch 54/100 on master with 2x-nproc load.
Add loadStoreIndexed (Load + WaitIndexesReady, which wakes on the ready
channel) and use it in every /paths test that loaded without waiting:
SortByRecency/SortCountTiebreaker_1145, the Kpa-clawbot#1352 collision scenario,
FallbackUniquePrefix_1352, HopName_1144 and PrefixCollisionExclusion.
AnchorBias_1278's 10 ms poll loop becomes the same helper.
After: 150/150 green under load for both named tests, 30/30 with -race.
Mutants: helper without the wait (29/60 red under load); no recency sort
(SortByRecency red); uniquePrefix always true (FallbackBranch red).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) main calls signal.Notify inside the shutdown goroutine, so /api/healthz can answer before that goroutine runs; a SIGTERM in that window takes Go's default action (exit -1, no "[server] received" line), the CI failure of TestServerProcess_NormalStartDoesNotWait. This guard parses main.go and requires the SIGTERM registration as a statement of main before httpServer.ListenAndServe. Red on master (Notify is in a func literal); the controls show it rejects a goroutine/go-statement/SIGINT-only registration and accepts the fixed order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
signal.Notify for SIGINT/SIGTERM ran inside the shutdown goroutine, while main went on to ListenAndServe. /api/healthz could answer before that goroutine was scheduled, and a SIGTERM in that window took Go's default action: exit -1 with no graceful shutdown (CI Go Server Tests 3/3 on #555, TestServerProcess_NormalStartDoesNotWait). Register synchronously before the goroutine starts, so healthz 200 implies the handler is installed. Probe on master (2 s before the goroutine's Notify): 3/3 exit -1. Same probe after the fix (before the now-synchronous Notify): 3/3 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) At 375px the packets page caps a saved time window above 3 h back to 15 min (#340, intentional), so the test's localStorage window of a year was ignored and the list had rows only while the freshened fixture's newest packet was under 15 min old. After ~30 min it showed "No packets found" and the wait for a row timed out. Pick a single-observation packet from an IATA-tagged observer via /api/packets and open #/packets?hash=<hash>, which suppresses the time window by contract (buildPacketsParams). The detail badge must now also show that observer's IATA. Red before: fixture aged 40 min, the mobile case times out. After: green on fixtures aged 40 min and 1 day. Mutants: detail Observer row without the badge, and a fixed IATA in the detail — both red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…567) The server resolves version/commit from ldflags, .git-commit or `git rev-parse` in its cwd. Without them both read "unknown", and renderVersionCard deliberately hides the card. The test treated "unknown" as present (!!"unknown"), so it waited for a card that never comes whenever the server could not resolve a commit: green in CI, red locally. Route /api/health through the real response, overriding only version, commit and a sentinel uptimeHuman (the Uptime card shows it, which marks the render the assertions read). Both branches are now asserted: unknown/unknown hides the card, and a known pair renders "v9.9.9-e2e · 01234567". The route is always removed afterwards. Red before: no-git server, timeout. After: green from a no-git and a git cwd; 30/30 under 2x-nproc load. Mutants: card shown for unknown, commit dropped, no card — all red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under CPU load the pinned mobile case still failed 6/30. The packets list
re-renders its rows after data-loaded (hop/observer decoration, ~10 ms and
~250 ms later), so:
- the held row handle was detached by click time ("Element is not
attached to the DOM"), and
- $eval resolves the cell and reads its style in two round trips; a cell
detached in between has computed display "" and read as "visible"
(verified: an attached hidden td reads "none", the same td detached
reads "").
Tap through a locator on the pinned row and read the observer cells and the
detail badge in single evaluates. The column check now also requires at
least one observer cell (the old .catch(() => false) passed with none).
Product code is unchanged: the column is always hidden at task boundaries
(0/30 runs with per-task polling).
30/30 green under 2x-nproc load on a 40-min-aged fixture. Mutants: detail
row without the badge, a fixed IATA in the detail, observer column made
tier-1 — all red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent1 PR#571 #567 — head 48d33d8Status: All five #567 items are fixed and locally green (N≥30 under CPU load, Evidence: [T] test or command run, with its result · [A] analysis by the author · [K] source code reference CPU load = 2×nproc busy loops on a 4-core linux/amd64 host. Requirements
[A] Product code is unchanged for #5. With per-task polling from page start, the observer cell was never visible at a task boundary (0/30 under load). The earlier "visible" readings were the detached-element read. Suites (local)
CI per job (run 40102051732, head
|
| Job | Result |
|---|---|
| Go Build & Test | pass |
| Go Server Tests (1/3, 2/3, 3/3) | pass |
| Go Ingestor Tests | pass |
| Go Libraries & CLI | pass |
| Verify Go Test Assignments | pass |
| JavaScript Unit Tests & Lint | pass |
| Script & Container Unit Checks | pass |
| Validate Docker Image | pass |
| Playwright E2E Tests (1/3), (2/3) | pass |
| Playwright E2E Tests (3/3) | fail: test-issue-497-rx-airtime-e2e.js "360x800: both tile stats stay inside the tile, each on one line" (oneLine:false) |
| Verify E2E Shards & Coverage | fail (needs E2E 3/3) |
| Build & Publish Docker Image | fail (needs the aggregate) |
| Publish Badges, Release Artifacts, Deploy Staging | skipped |
Failure analysis:
- [T] The latest master run on this PR's base
b3fc76da(run 40097467456) failed the same shard in the same file: "a very narrow tile wraps between stats …oneLine:false". The master run before it passed. Both runs used the same runner OS (Ubuntu 24.04.5), so this is an intermittent layout flake in feat(observers): show RX 1h airtime (channel load) next to TX 1h, and both on the observer detail page #497's narrow-tile checks. - [K] This PR changes no frontend, CSS or feat(observers): show RX 1h airtime (channel load) next to TX 1h, and both on the observer detail page #497 file (
git diff origin/master --stat -- public test-issue-497-rx-airtime-e2e.jsis empty). - It is not flaky: #180 packets URL E2E 'reload shows the same list' depends on fixture age (default 15-min window) #271, so under the agreed rule the job was not re-run. Re-running it is the maintainer's call.
Hygiene
- [T] No closing keywords in the title, body or commits ("Relates to tests: deflake pre-existing flaky Go and E2E tests (index-loading 503, relay-info race, start-up race, fixture age) #567"). All 8 commits are authored and committed by
dborup <kontakt@meshview.dk>. No rebase, amend or force-push. - [T]
scripts/check-xss-sinks.sh --diff origin/master: nopublic/changes to scan. No hardcoded colours (the only#…hits are issue references). - [T]
map[string]interface{}added in non-test Go: 0. [K]cmd/serverstays read-only: the only product change is signal registration order. - [T] No
.githubdiff; fork guards are unchanged: 9 indeploy.yml, 1 inrelease-fast-path.yml. - No staging or production access was used. region-filter: cross-entry Back, E2E API-count flake, history.state pin (follow-up to #549) #556 / region-filter: cross-entry Back, E2E flake, history.state pin #570 are untouched.
Remainders
test-issue-497-rx-airtime-e2e.jsnarrow-tile layout flake (master and this PR, CI only; 5/5 local). It deserves its own issue. Not filed (outside this mandate).- [A] A SIGTERM during start-up before the listener binds (between the schema wait and the shutdown registration) still takes the default action. That is unchanged behaviour and not this flake. Deferring termination during a long load would be a behaviour change, so it is left as is.
- [A] The comment in
public/packets.js(fix(#1692): parallelize loadObservers + loadPackets in /packets init() Kpa-clawbot/CoreScope#1693) attributes the earlier mobile flake to the MutationObserver debounce. The remaining cause was the detached-element read in the test. The comment was not edited, since this PR makes no frontend change. - [K] Only linux/amd64 was measured. The relay flake was reported at 66/150 on darwin/arm64. Its root cause (a cold node cache in the oracle) is platform-independent, but arm64 was not re-measured here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r read (#567) TestTrackedBytes_MixedSequenceDoesNotDrift_113 failed on master 273c216 with "database is locked (5) (SQLITE_BUSY)" on a fixture INSERT. The fixture asks for WAL with "?_journal_mode=WAL", which modernc.org/sqlite ignores (it applies only _pragma, _time_format and _txlock), so the file is in the rollback journal (PRAGMA journal_mode = delete) and neither connection has a busy timeout. The loaded store's schema healer re-probes this schema every 2 s for its missing optional columns; a commit that meets that SHARED lock fails at once. The new test holds a read open on the server's connection and writes through a fixture connection: red, SQLITE_BUSY, every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#567) The acct113 fixture asked for WAL with "?_journal_mode=WAL", a DSN parameter modernc.org/sqlite ignores. The file stayed in the rollback journal, and no fixture connection had a busy timeout. Tests write to the fixture while a loaded store reads it. The store's schema healer re-probes this schema every 2 s, because the fixture lacks optional columns. A commit that met one of those reads failed with SQLITE_BUSY. That was 24/150 under 2x-nproc load with -race on the parent commit, and with the healer forced to a 1 ms tick it was 10/10. acct113OpenWriter opens the fixture the way cmd/ingestor/db.go opens the real database: _pragma=journal_mode(WAL) and _pragma=busy_timeout (5000). It reads both pragmas back, because a parameter that is silently dropped is exactly what broke here. WAL alone is not enough. A reader still holds a WAL lock for a moment during the wal-index header check, so with the forced 1 ms tick WAL without a busy timeout was still red (3/30, 10/60). With both it was 0/60. Every writer of this fixture uses the helper: create, the mixed sequence, leak202Insert, the two ingest benchmarks and hm215Create. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"a very narrow tile wraps between stats" failed in CI (master b3fc76d) with {"tid":"avg: -111.0","nowrap":false,"inside":true,"oneLine":false}, and the 360x800 tile check failed on #571 with {"inside":true, "oneLine":false}. That is the reading of a detached tile: the rect is 0 (so inside), and the computed style is empty, so whiteSpace is "" and fontSize is NaN (so oneLine is false). An attached tile reads nowrap:true. locator.evaluate resolves the tile (waitForSelector, attached) and runs the function on the handle in a second round trip. RF Health rebuilds the grid with innerHTML on every theme-refresh and region/area change, so a rebuild in between measured the old, detached tile. Late CSS is ruled out. style.css is a head <link> that scripts wait for, and the RF Health gotos are same-document hash navigations, so the sheet was applied long before. Holding style.css back 3 s kept the file green. measureTile finds and measures the tile in one waitForFunction task and waits until the tile exists. The assertions are unchanged. Forced re-render (theme-refresh every 15 ms on the 360 px tests): before 5/5 red with the CI signature, after 8/8 green. 30/30 green under 2x-nproc CPU load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent1 PR#571 runde-2 — head d176af1Review feedback addressed (commit
Evidence: [T] test or command run, with its result · [A] analysis by the author · [K] source code reference CPU load = 2×nproc busy loops on a 4-core linux/amd64 host. 1. RX-airtime narrow tile (E2E)Root cause.
The stylesheet hypothesis is ruled out.
Fix.
[A] Locally the grid was not rebuilt between the gotos (trace: one rebuild, caused by the test's own theme-refresh). The CI-side trigger is therefore one of the rebuild paths above landing in the window, and it was not pinned down further. The fix does not depend on which path it is. 2.
|
| Result | |
|---|---|
| Red before | [T] Original test 24/150 red under load with -race (all SQLITE_BUSY at the three INSERT sites, including master's line 448); 1 ms tick 10/10; new test red every run |
| Green after | [T] 150/150 for the mixed sequence and 150/150 for the new test, under load with -race, 0 DATA RACE (26 min); 1 ms tick 0/60 |
| Mutants (all killed) | [T] G1 old DSN ?_journal_mode=WAL&_busy_timeout=5000: read-back red (journal_mode = "delete"). G2 _pragma=journal_mode(WAL)&_busy_timeout=5000: read-back red (busy_timeout = 0). G3 rollback journal + busy timeout, read-back disabled: the new test is red (writer blocked 5 s, then BUSY). G4 WAL without busy timeout, read-back disabled: 1 ms tick 3/30 and 10/60 red |
Suites (local, head d176af18)
- [T]
cd cmd/server && go vet ./... && go test ./...: ok.cd cmd/ingestor && go test ./...: ok. - [T] The 59 acct113/202/215/490 tests: ok.
-bench TrackedBytes -benchtime 1x: all four benchmarks run. - [T]
sh test-all.sh: 267 passed, 0 failed.node test-frontend-helpers.js: 719 passed, 0 failed. - [T] E2E against a local Go server on
e2e-fixture.dbprepared as in CI (freshen, inline seed SQL,corescope-migrate, seeds 2073/199/245/122):test-issue-497-rx-airtime-e2e.js: 16/16.test-observer-iata-1188-e2e.js: 3/3.test-e2e-playwright.js: 154/157 (3 built-in skips, 0 failed).
CI per job (run 40110929120, head d176af18, attempt 1, no re-runs)
| Job | Result |
|---|---|
| Go Build & Test | pass |
| Go Server Tests (1/3, 2/3, 3/3) | pass (3/3 ran MixedSequenceDoesNotDrift_113 and ServerReadDoesNotBlockWriter_567, both pass) |
| Go Ingestor Tests | pass |
| Go Libraries & CLI | pass |
| Verify Go Test Assignments | pass |
| JavaScript Unit Tests & Lint | pass |
| Script & Container Unit Checks | pass |
| Validate Docker Image | pass |
| Playwright E2E Tests (1/3), (2/3), (3/3) | pass (3/3: both #497 tile tests ✅, "All #497 … passed") |
| Verify E2E Shards & Coverage | pass |
| Build & Publish Docker Image | pass |
| Release Artifacts, Deploy Staging, Publish Badges & Summary | skipped (branch run) |
#271 and #301 did not fail, so no job was re-run.
Hygiene
- [T] Commits are
02d33f88(merge),1c69b026,525f1daaandd176af18. All 12 commits on the branch are authored and committed bydborup <kontakt@meshview.dk>. - [T] No rebase, amend or force-push; the push was a fast-forward
48d33d8c..d176af18. No closing keywords. - [T] Round 2 changes only test files: 5 Go
_test.gofiles and the feat(observers): show RX 1h airtime (channel load) next to TX 1h, and both on the observer detail page #497 E2E. Nopublic/or.githubdiff.check-xss-sinks.sh --diff: nothing to scan.map[string]interface{}added in non-test Go: 0. Fork guards unchanged (9 indeploy.yml). - No staging or production access. No API key used. No upstream writes.
Remainders
- [K] The same ignored-parameter pattern exists in product code.
cmd/server/db.goOpenDB(mode=ro&_journal_mode=WAL&_busy_timeout=5000),cmd/server/ping_score_history.go:451andcmd/ingestor/admin_delete_client_rx.go:104pass mattn-style parameters that modernc ignores. [T] The server's read connections therefore run withbusy_timeout=0. [A] In production the ingestor keeps the DB in WAL, so readers are rarely blocked, but a transient WAL lock still fails a server read at once instead of waiting. This is a product change and is left out of this test-only round. It deserves its own issue (not filed). - [K] 27 other
cmd/servertest files still open SQLite with?_journal_mode=WAL. [A] Any of them that writes while a loaded store reads carries the same latent hazard. They were not measured or changed here, to keep the scope on the reported flake.acct113OpenWriteris the pattern to reuse. - [A] For item 1, the exact CI-side rebuild trigger (theme-refresh, region or area change) was not identified. The fix holds for any rebuild.
- Only linux/amd64 was measured.
Review — CS-Minimax PR#571 — head d176af1Dom: APPROVE med nits Evidence: [T] test or command I ran, with its result · [A] my analysis · [K] source code reference All 7 items have a proven root cause and a red-before / green-after test. No assertion is weakened (items 4 and 5 are stricter now), and no sleep or retry was added. The one product change (signal registration order) is minimal and correct. The nits below are not blocking. One follow-up issue is recommended (finding 3). Local platform: darwin/arm64, 12 cores, Go 1.26.0. "Under load" means 12 busy loops (1×nproc). The head was unchanged before and after the review ( Findings
Answers to the review pointsItem 1, relay oracle (test).
Item 2,
Item 3,
Item 4, Version card (test).
Item 5, observer IATA on mobile (test).
Item 6, narrow tile / detached tile (test).
Item 7, SQLITE_BUSY in the acct113 fixture (test) and production exposure.
Always-checks
Tests run
Not verified
|
Relates to #567
Makes the pre-existing flaky tests from #567 deterministic. For each one I found the root cause first. One of them is a real product race and is fixed in the product. The others are test races, fixed by waiting for a real readiness signal or by removing a wall-clock/cwd dependency. No assertion is weakened, and no sleep or retry is added. The #556 API-count flake (
test-issue-531-region-url-e2e.js) is handled in #570 and is not touched here.Plan and root causes
TestRepeaterRelayInfoMap_ConcurrentIngestAndEvictioncollectRelayEntriesLocked(key)without a metadata snapshot, so it read the node cache, which is nil until the first bulk reader warms it. The round-0 oracle therefore credited no relay, while every bulk read credited 100 per relay. A reader that finished before round 1 "matched no store state".snapshotPathNodes), the wayGetRepeaterRelayInfodoes, and assert that the round-0 oracle carries relay evidence.TestHandleNodePaths_SortByRecency_1145,TestHandleNodePaths_PrefixCollision_1352_FallbackBranch(+ same-shaped siblings)Load()builds the path-hop index in the background and/pathsanswers 503 until it is ready (Kpa-clawbot#1008, by design). The tests queried right afterLoad().loadStoreIndexedhelper (Load+WaitIndexesReady, which wakes on the ready channel). Used by every/pathstest that loaded without waiting;AnchorBias_1278's 10 ms poll loop becomes the same helper.TestServerProcess_NormalStartDoesNotWaitmaincalledsignal.Notifyinside the shutdown goroutine, so/api/healthzcould answer before that goroutine ran. A SIGTERM in that window took Go's default action: exit -1, no graceful shutdown.ListenAndServe. Guarded by an AST test.test-e2e-playwright.js).git-commitor a git cwd,/api/healthreports"unknown", andrenderVersionCarddeliberately hides the card. The test treated"unknown"as present (!!"unknown") and waited for a card that never comes./api/healththrough the real response, overriding onlyversion/commitplus a sentineluptimeHuman. The sentinel shows up in the Uptime card and proves which response was rendered. Both branches are asserted (hidden for unknown/unknown, exact text otherwise). The route is always removed.test-observer-iata-1188-e2e.js)data-loaded, so a held row handle could be detached at click time, and$evalcould read a detached<td>, whose computeddisplayis"", so it counted as "visible".#/packets?hash=…, which suppresses the time window by contract. Click via a locator, and read cells and the badge in oneevaluate. Also stronger: require ≥1 observer cell (the old.catch(() => false)let "no cell" pass) and require that the detail badge equals the observer's IATA.test-issue-497-rx-airtime-e2e.js)locator.evaluateresolves the tile and runs the function on the handle in a second round trip. RF Health rebuilds the grid withinnerHTMLon every theme-refresh and region/area change, so a rebuild in between measured a detached tile. A detached element has a zero rect and an empty computed style, which reads exactly as the CI failures did:{"nowrap":false,"inside":true,"oneLine":false}. Late CSS is ruled out, becausestyle.cssis a head<link>that scripts wait for and the RF Health gotos are same-document hash navigations. Really missing CSS readsoneLine:true, notfalse.measureTilefinds and measures the tile in onewaitForFunctiontask and waits until the tile exists. The assertions are unchanged.TestTrackedBytes_MixedSequenceDoesNotDrift_113(SQLITE_BUSY)?_journal_mode=WAL, a DSN parameter thatmodernc.org/sqliteignores. The file stayed in the rollback journal, and no fixture connection had a busy timeout. The loaded store's schema healer re-probes this schema every 2 s, because the fixture lacks optional columns. A fixture commit that met that read failed at once.acct113OpenWriteropens the fixture the waycmd/ingestor/db.goopens the real database (_pragma=journal_mode(WAL)+_pragma=busy_timeout(5000)) and reads both pragmas back. Every writer of the fixture uses it. NewTestAcct113Fixture_ServerReadDoesNotBlockWriter_567holds a server read open while the fixture writes.Tests and evidence
CPU load means 2×nproc busy loops on a 4-core linux/amd64 host.
-raceRUnlock: DATA RACE under-race; 2 mismatches + 1 panic in 150 without it-race; all 11/pathstests 30/30 under loaduniquePrefix := true(FallbackBranch red)Notifyreproduces the CI signature 3/3 (exit -1, no "received" line)-race; the same 2 s delay before the now-synchronousNotify: 3/3 passNotifyback in the goroutine (master);go signal.Notify(…); guard controls cover goroutine / go statement / SIGINT-onlywhite-space: nowrapremoved from.rf-cell-stats > span;flex-wrapremoved;measureTilewithout the null guard (5/5 red under re-render)-race; healer forced to a 1 ms tick: 10/10-race, 0 data races; 1 ms tick: 0/60_journal_mode=WAL,_busy_timeout=5000): read-back red; rollback journal + busy timeout: writer-during-read test red; WAL without busy timeout: 1 ms tick 3/30 and 10/60 redSuites:
cmd/serverandcmd/ingestorgo test ./...,sh test-all.sh,node test-frontend-helpers.js, and all three changed E2E files against a local Go server one2e-fixture.db, prepared as in CI. Results are in the report comment.Notes
origin/master(273c2162).cmd/serverstays read-only. No newmap[string]interface{}, no frontend or CSS change, no.githubchange. Fork guards are unchanged (9 indeploy.yml, 1 inrelease-fast-path.yml).cmd/server/db.goOpenDBalso passes_journal_mode=WAL&_busy_timeout=5000, which modernc ignores, so the server's read connections have no busy timeout. That is a product question and is not changed here (see the report).public/packets.js(fix(#1692): parallelize loadObservers + loadPackets in /packets init() Kpa-clawbot/CoreScope#1693) attributes the earlier mobile flake to the MutationObserver debounce. The detached-element read described above is what remained. Product code is unchanged.🤖 Generated with Claude Code