Deflake the unit-test cluster: seed HNSW routing, signal-based waits, quarantine two unreproduced hangs - #2276
Deflake the unit-test cluster: seed HNSW routing, signal-based waits, quarantine two unreproduced hangs#2276kriszyp wants to merge 4 commits into
Conversation
…subscription waits, quarantine 2 unreproduced flakes Fixes three flaky unit tests with demonstrated root causes and quarantines two that could not be root-caused this round (each linked to a filed issue): - HNSW greedy-routing test: graph levels came from unseeded Math.random, and greedy-vs-full-ef equality is only statistically true across random graphs. Adds a 'random' test seam to HierarchicalNavigableSmallWorld (also clamps the entry-point level to MAX_LEVEL, matching the other assignment site) and seeds the test's graph. 0/120 contended runs post-fix (was ~2.5-5%). - Txn expiration test: fixed 50ms window raced a 40ms sleep plus real DB work plus two 20ms expiry ticks. Now waits on the actual signals and proves expiry landed before the slow get() settled. 0/160 pinned runs (was 2/80). - Subscription replay (updates to passed keys): still used collect()'s quiet-period timer, the race its sibling tests were already migrated off; now uses the same waitFor-final-values pattern. - risk-query integration suite: skipped on win32 (#2273 — deploy_component hangs after npm pack on Windows CI) and readiness-poll fetches now carry AbortSignal.timeout so one hung fetch cannot burn undici's 300s default. - MQTT non-clean-session test: skipped (#2274 — silent 20s hang on CI, 0/30 contended local repro attempts). Refs #1655 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142195pzooHeNejheNfsPDZ
…t-period waits, narrow MQTT skip to lmdb, test the entry-point clamp
- txn-tracking: await the slow get() (asserting the abort surfaces on
rocksdb) so its 500ms tail cannot bleed into the next describe's
expiration settings and re-pathed test DB.
- subscriptionReplay: convert the two remaining quiet-period/fixed-delay
waits ('rapid updates' and 'subscribe while writes are in flight') to
the same waitFor pattern; trim history-narrating comments.
- mqtt-test: quarantine the non-clean-session test on lmdb only (where
the hang was observed) so rocksdb keeps the durable-session coverage.
- vectorIndex: regression test pinning the empty-index entry-point level
clamp (random() === 0 previously meant an infinite loop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142195pzooHeNejheNfsPDZ
…ect() quiet windows - vectorIndex: drive the clamp test with Number.MIN_VALUE (finite ~268 level) instead of 0 — a clamp regression now fails in milliseconds rather than wedging the runner in a synchronous infinite loop. - subscriptionReplay: convert the last three collect() quiet-window waits; the two duplicate-detection tests could previously pass vacuously when the window expired before in-flight deliveries. The non-collection test waits for the final version only, since rapid same-record versions legitimately coalesce. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142195pzooHeNejheNfsPDZ
… tests A duplicate can trail the last expected delivery; the 100ms settle after the positive wait can only surface more events, never lose them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142195pzooHeNejheNfsPDZ
There was a problem hiding this comment.
Code Review
This pull request addresses test flakiness and environment-specific hangs by introducing condition-based polling (using a waitFor helper), adding abort timeouts to fetch calls, and quarantining problematic tests on Windows and LMDB. It also introduces a random function seam in the HNSW index to allow seeding the PRNG for deterministic graph generation in tests, and clamps the entry-point level to MAX_LEVEL. The reviewer feedback recommends replacing the remaining fixed delays in subscription replay tests with a sentinel record pattern to ensure all prior events are fully processed before assertions, which aligns with best practices for testing ordered streams.
|
Reviewed; no blockers found. One non-blocking suggestion posted inline. |
Deflakes the cluster of unit-test failures that took the Unit Test workflow red on 7 of its last 30 main runs (four distinct tests across 2026-08-21 alone), plus the Windows-only risk-query integration failure. Three tests are fixed with demonstrated root causes; two could not be root-caused within this round and are quarantined, each behind a filed, prioritized issue under epic [Epic] CI test flakiness (#1655). No product behavior changes beyond a test seam (details below).
What changed
Fixed (root cause demonstrated):
unitTests/resources/vectorIndex.test.js,resources/indexes/HierarchicalNavigableSmallWorld.ts) — graph levels were drawn from unseededMath.random(), and greedy-vs-full-ef result equality is only statistically true across random graphs: ~2-3% of random 600-node graphs legitimately route to a different layer-0 entry point and displace the top-10 tail. Added arandomproperty toHierarchicalNavigableSmallWorld(test seam, defaults toMath.random) and the test now pins the graph with a seeded PRNG. Product-side this also clamps the entry-point-creation level toMAX_LEVEL, matching the other assignment site — previously-Math.log(random())at that one site was unclamped, and arandom()returning exactly 0 (possible for bothMath.randomand any test PRNG) would producelevel = Infinityand an infinite loop in the node-initializationforloop; a high finite draw persists an entry point dozens of empty layers above the graph for the life of the index. And it is not just a lottery ticket: with a schema-configuredmL(e.g.mL: 5, which the constructor accepts), 13.5% of first-node draws exceed MAX_LEVEL — every such index got a permanently over-tall entry point while later nodes were clamped (review-credit: the harper-domain leg quantified this). The clamp now has its own regression test (HNSW entry-point level clamp), driven with a finite pathological draw so a clamp regression fails fast instead of wedging the runner. Also recorded the statistical nature of greedy-equals-full inDESIGN.md.unitTests/resources/txn-tracking.test.js) — the test raced a fixed 50ms window (Promise.race([delay(50), result])) against a 40ms sleep + two real DB operations + an expiry that structurally needs two ~20ms monitor ticks. Ten milliseconds of slack loses on a contended runner. Now waits on the actual signals (waitFor), and proves the transaction lefttrackedTxnswhile the slowget()was still pending — so removal-by-expiry can't be confused with removal-by-completion.unitTests/resources/subscriptionReplay.test.js) —!omitCurrent: updates to passed keys arrive via queuestill usedcollect()'s quiet-period timer, the exact race its two sibling tests were already migrated off (their in-file comments document it). Under contention the subscription can go quiet longer than the window while queued updates are in flight, so the final-value assertion reads stale values. Now attaches a listener andwaitFors every key's final value, with the timeout falling through to the per-key asserts for precise failure messages. The pre-push review then caught five more tests in the file still carrying the same quiet-period/fixed-delay pattern — including two duplicate-detection tests whose assertions could pass vacuously when the quiet window expired before in-flight deliveries arrived. All five are converted, so the file'scollect()quiet-window idiom is fully retired for in-flight-write tests (it remains only where a quiet window is the semantically right tool, e.g. asserting no events arrive).Quarantined (issue filed, skip links to it):
integrationTests/components/risk-query.test.ts) — #2273:deploy_component(restart:true) hung server-side afternpm packon Windows CI — the instance log goes silent and the client fetch dies on undici's 300s headers timeout, cancelling all 9 children at 319s. This is a hang, not runner slowness (npm pack itself finished in <1s; then 5+ minutes of zero log output), so a timeout bump would not help — the suite is skipped onwin32until the deploy hang is fixed. The readiness poll also now carriesAbortSignal.timeout(5s)per probe so one hung fetch can't consume the whole budget. Suite verified green on Linux.unitTests/apiTests/mqtt-test.mjs) — #2274: silent 20s mocha timeout on CI (lmdb pass, Node 26), zero server-side log output, and the hang is provably in one of the steps with no individual timeout (the test's own bounded 15s wait never fired). Not reproduced in 30 contended local full-file runs. Skipped on the lmdb pass only — where the hang was observed — so the rocksdb pass keeps this durable-session coverage (this narrowing was a pre-push review finding; the first cut skipped it everywhere). Hypotheses and a reinstatement path are in the issue. Most of this file's diff is indentation from wrapping the test in the conditional skip; the body is unchanged.Found but not touched (out of scope): the repro loops surfaced a sixth flaky test,
MQTT subscribe to retained record with patch operations— ~10% failure rate under 2-core contention, duplicate retained/live delivery asserted as an uncaught exception. Filed as #2275 with mechanism and repro instructions.For the human reviewer
HierarchicalNavigableSmallWorld.ts, and the judgment call is therandominstance property as a test seam plus theMAX_LEVELclamp on the entry-point site. The seam is inert in production (this.randomdefaults toMath.random; both call sites previously calledMath.random()directly). The clamp changes behavior only forrandom() === 0or graphs demanding level > 10, i.e. probability ~2^-32 per insert — but the unclamped site could infinite-loop, so it is a genuine (if theoretical) hardening, called out here rather than buried.get()completes before expiry is observed, which would indicate the timing assumption broke in a new way rather than passing vacuously.waitFortimeout (.catch(() => {})) so the detailed per-key asserts report exactly which key went stale — a deliberate trade of one generic error for precise diagnostics.randomseam as a public field vs threading through schemaoptions(field — options are persisted schema config, a function doesn't round-trip); MAX_LEVEL clamp bundled here vs its own PR (bundled — one line, named in the commit body, now with its own regression test); quarantine now vs holding for root cause (quarantine — greening main is the task).get()caller, and drains the slow operation so its 500ms tail can't bleed into the next describe — a round-1 review finding.Verification
dist/, and the seam wasn't built yet; afternpm run build, graph state (level histograms) is byte-identical across runs. Diagnostics also confirmed layer-0 connectivity and identical failure output pre-fix, ruling out ef auto-scale flapping.collect()for exactly this race; post-fix 0/72 single-core.harper.js install,DEFAULTS_MODE=dev): target test 0 failures (hence quarantine rather than a speculative fix), sibling patch-operations test 3 failures (filed Flaky unit test: MQTT 'subscribe to retained record with patch operations' — duplicate retained/live delivery fails as uncaught assertion (~10% under contention) #2275).npm run test:unit:main,test:unit:resources(rocksdb and lmdb), full apiTests pass (rocksdb, sandboxed fresh install),integrationTests/components/risk-query.test.ts(9/9 on Linux),lint:required, prettier.test:integration:allnot run in full: this branch touches a single integration file (run individually above) and the Integration workflow on main is currently failing for unrelated reasons (17 of its last 30 main runs), so a full local run has no usable baseline.Refs [Epic] CI test flakiness (#1655)
Complexity: low — test-only changes plus one inert test seam and a theoretical-edge clamp in HNSW level assignment.
— Claude Fable 5
🤖 Generated with Claude Code
https://claude.ai/code/session_0142195pzooHeNejheNfsPDZ
Review-Coverage: authored=claude; ran=codex; blocked=gemini(exit-1); declined=cursor-grok,cursor-composer,domain; rounds=4 @ cf75953
Human-Review-Need: 4 @ cf75953