You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
waitForCondition in tests/e2e/sdk/helpers.ts cannot enforce the budget it advertises. One slow fn() overruns it arbitrarily, so a latency spike surfaces as an opaque vitest Test timed out in 20000ms instead of a legible Condition not met after 6000ms.
This is a diagnosability defect, not a correctness one. The suite still passes on a machine with spare CPU, and on CI. What it costs is debugging time when something is slow — the failure names nothing, so the reader's first instinct is to suspect their own diff.
Re-scoped 2026-08-11. This issue was originally filed as "local make ci blocked" during #446. That framing was wrong: the frequency of those failures was environmental (CPU contention on one developer's machine — see "What this is not" below), and no repo change would have prevented them. The residue is the budget bug described here, which is machine-independent and worth fixing on its own merits. Priority dropped P2 → P3 to match.
The bug
exportasyncfunctionwaitForCondition(fn,timeoutMs=10_000,intervalMs=250){conststart=Date.now();while(Date.now()-start<timeoutMs){if(awaitfn())return;// ← unbounded: the deadline is checked// BEFORE the call, never during itawaitnewPromise((r)=>setTimeout(r,intervalMs));}thrownewError(`Condition not met after ${timeoutMs}ms`);}
The loop condition guards entry, not the call. Nothing interrupts fn() once it starts, so the effective ceiling is timeoutMs + (duration of the last fn()), unbounded.
Instrumented evidence from a real run:
poll#8 -> false (fn took 246ms, elapsed 4506ms)
poll#9 -> true (fn took 12716ms, elapsed 17472ms) # 6s budget, returned at 17.5s
poll#8 -> true (fn took 23259ms, elapsed 28019ms) # 10s budget, returned at 28.0s
A 10s budget ran 28s. Because that overshoots the caller's vitest testTimeout, the throw never happens — vitest kills the test first, and the resulting message says nothing about which condition failed or how long the poll actually waited.
chQuery (same file) has no timeout either, so it can be the slow call indefinitely.
Suggested fix
Race fn() against the remaining budget rather than checking the clock only between polls, so the documented Condition not met after <n>ms actually fires at <n>.
Give chQuery an AbortSignal.timeout(...) so a single stalled request fails as itself rather than as a mystery.
Neither makes anything faster. Both convert a silent wall into a failure that names its own cause.
Why it's worth doing anyway
The e2e suite's timing assumptions are tight enough that a merely busy machine can brush them, independent of any pathological process:
The 500-row test's own comment records 5104ms observed on APFS for the JetStream publish-fsync, against a 2500ms ack assertion elsewhere in the same file.
A healthy control run measured that fsync at 3763ms — real but not generous headroom.
When any of those fire, the current code turns a five-second diagnosis into a long one.
What this is not
SUPERSEDED 2026-08-11 — this section's conclusion was wrong. The failures were not
environmental. Root cause: undici 8.8.0-8.9.0 stalls for seconds before writing a request onto an
idle pooled socket (nodejs/undici#5600, fixed in 8.10.0); Node 26 bundles 8.9.0 while .nvmrc
pins Node 22 for CI. Local make test-e2e went 3-fail-in-5 to 0-fail-in-5 with a one-line change.
The measurements and reasoning below are left intact for the record, but see this comment first.
Recorded so the next reader doesn't repeat the investigation. The original burst of failures was CPU contention on one developer's machine, not a repo defect:
Evidence
Reading
Second developer, same hardware/OrbStack, fresh clone: suite green in 111.6s, the victim test at 5111ms
Code fine
GitHub CI green — including 4 of 5 times the same tree failed locally
Same queries during a loaded run: 2.5–17.8s, 14 stalls totalling 89.4s
Contention
Load average 8.54 mean on a 14-core machine, one process averaging 82% of a core (203% peak)
Cause
Ruled out with measurements, not reasoning: stale/reused containers (fresh testcontainer per run), Docker/OrbStack health, host disk pressure (295 GB free), the ingest path (single-row insert acked in 10ms; server logged it 519ms after the preceding batch), connection churn, and TCP SYN drops.
The moving target — a different test failing each run — is the signature of contention against the tightest-bounded assertion, not of a specific broken test. batching.test.ts was the most frequent victim only because its 20s ceiling is the tightest.
Reproduction
Not reliably reproducible on an idle machine, by design — that's the point. To see the overrun directly, instrument waitForCondition to log fn() duration per poll and run the suite under artificial CPU load.
waitForConditionintests/e2e/sdk/helpers.tscannot enforce the budget it advertises. One slowfn()overruns it arbitrarily, so a latency spike surfaces as an opaque vitestTest timed out in 20000msinstead of a legibleCondition not met after 6000ms.This is a diagnosability defect, not a correctness one. The suite still passes on a machine with spare CPU, and on CI. What it costs is debugging time when something is slow — the failure names nothing, so the reader's first instinct is to suspect their own diff.
The bug
The loop condition guards entry, not the call. Nothing interrupts
fn()once it starts, so the effective ceiling istimeoutMs + (duration of the last fn()), unbounded.Instrumented evidence from a real run:
A 10s budget ran 28s. Because that overshoots the caller's vitest
testTimeout, the throw never happens — vitest kills the test first, and the resulting message says nothing about which condition failed or how long the poll actually waited.chQuery(same file) has no timeout either, so it can be the slow call indefinitely.Suggested fix
fn()against the remaining budget rather than checking the clock only between polls, so the documentedCondition not met after <n>msactually fires at<n>.chQueryanAbortSignal.timeout(...)so a single stalled request fails as itself rather than as a mystery.Neither makes anything faster. Both convert a silent wall into a failure that names its own cause.
Why it's worth doing anyway
The e2e suite's timing assumptions are tight enough that a merely busy machine can brush them, independent of any pathological process:
5104ms observed on APFSfor the JetStream publish-fsync, against a2500msack assertion elsewhere in the same file.When any of those fire, the current code turns a five-second diagnosis into a long one.
What this is not
Recorded so the next reader doesn't repeat the investigation. The original burst of failures was CPU contention on one developer's machine, not a repo defect:
Ruled out with measurements, not reasoning: stale/reused containers (fresh testcontainer per run), Docker/OrbStack health, host disk pressure (295 GB free), the ingest path (single-row insert acked in 10ms; server logged it 519ms after the preceding batch), connection churn, and TCP SYN drops.
The moving target — a different test failing each run — is the signature of contention against the tightest-bounded assertion, not of a specific broken test.
batching.test.tswas the most frequent victim only because its 20s ceiling is the tightest.Reproduction
Not reliably reproducible on an idle machine, by design — that's the point. To see the overrun directly, instrument
waitForConditionto logfn()duration per poll and run the suite under artificial CPU load.