fix(test): enforce e2e poll budgets and stop reusing idle connections - #455
Conversation
Two defects, one of which was hiding the other. waitForCondition checked the clock only on loop entry, so one slow fn() overran the budget without bound — a 10s budget was measured running 28s, past the caller's testTimeout, so vitest killed the test first and reported a timeout naming neither the condition nor how long the poll waited. It now races fn() against the deadline, aborts the in-flight call, and reports poll shape: "N poll(s), slowest Xms". That reporting is what exposed the second defect. chQuery used the global fetch, which reuses pooled connections; undici 8.8.0-8.9.0 stalls for seconds before writing a request onto a socket idle for a few seconds (nodejs/undici#5600, fixed in 8.10.0). This suite has multi-second idle gaps by construction — the 5s ingest linger sits between every write and the first poll of its visibility wait — so every visibility wait sat in the triggering window. Node 26 bundles undici 8.9.0; CI runs Node 22 (undici 6.28.0) via .nvmrc, which is why CI never saw it. Local `make test-e2e`: 2 pass/3 fail -> 5 pass/0 fail, and every run faster (115.7-124.5s vs 128.7-137.1s). ClickHouse measured p99 <= 2.5ms throughout; Docker/OrbStack, DNS, the libuv threadpool, GC, event-loop blocking and machine load were each excluded by measurement. See #440. Also here: - chQuery gets a 10s ceiling and honours the caller's AbortSignal, threaded through 19 call sites, so an abandoned poll tears its request down. - batching's visibility wait had ~700ms of headroom over the 5s linger while every other wait allows 10s; widened. The >= 4500ms lower bound, which is what the test actually asserts, is unchanged. - The e2e banner prints node/undici and warns when the local major differs from .nvmrc, so the next version-specific failure is attributable in seconds rather than days. - The orchestrator refuses to start beside an orphaned wavehouse-cov. A killed run leaves one, and it corrupts the next run through the shared tmp/data and log file — presenting as a dozen unrelated tests failing to see their rows, in a log blaming a container that no longer exists. - vitest.config: __dirname -> import.meta.dirname, silencing the vite 8 configLoader warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
@wavehouse/sdk advertised node >=18. Nothing tests 18, and both 18 and 20 are past end-of-life upstream — so the floor promised to consumers was neither supported nor backed by evidence. The rest of the workspace already requires >=22, and .nvmrc pins 22 for CI, so 22 is the oldest line that is actually exercised. Docs updated to match, since the runtime-support section quoted the old minimum verbatim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
Review follow-up on the stale-server guard. Refusing to start is right on a laptop, where the message names the PID and the kill command — but on a shared runner a process orphaned by a canceled job would wedge every subsequent e2e run until someone got shell access. A match is by construction this repo's own cover binary from a dead run, and the very next statement wipes tmp/data out from under it regardless, so killing it is both safe and what the guard was protecting against. Logged loudly; a kill that fails still aborts with the manual command. Also loosens the poll-stat assertion in helpers.test.ts, which put a 999ms *upper* bound on a wall-clock measurement taken while ClickHouse and the server share the machine — the exact shape of flake this branch exists to remove, and against the file header's own "order-of-magnitude, not milliseconds" rule. The property under test is that the in-flight poll was counted at all, so a lower bound is sufficient. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
Documentation sync for the two preceding commits. CHANGELOG gains [Unreleased] entries for both. It needed them badly: nothing in this repo has been released, so the Unreleased section *is* the shipping description, and it still advertised "engines.node is relaxed from >=22 to >=18 ... no longer warns or fails to install on Node 18/20" — the exact opposite of what now ships. That bullet is annotated as superseded rather than rewritten, so the decision history stays readable. pnpm-workspace.yaml's engineStrict rationale was the last place still asserting the two-tier ">=18 for consumers, >=22 for us" policy. development.md described an E2E harness that does not exist: a setup.ts that "probes ports before starting Docker services" (it probes, then throws) and a `make dev` detection that reuses a healthy :8080 (the orchestrator always provisions its own stack on a random port). Replaced with what the code does, plus the supported way to run vitest against a hand-run stack, the new leftover-server behavior, and a table of the env knobs this branch adds (E2E_CH_QUERY_TIMEOUT_MS, E2E_NO_COVERAGE) alongside the existing V=1. The two exhaustive E2E test-file lists (development.md, sdk/reference.md) gained helpers, noting it is a stack-free unit test of the harness rather than a pipeline test, so it doesn't read as inconsistent with the surrounding "exercises the full pipeline" claim. clients/ts/README.md is the page npm renders, and stated no Node requirement at all — a consumer on 20 now hits EBADENGINE with nothing to explain it. sdk/queries.md dropped a "Node 20+" qualifier that sits below the supported floor, and sdk/index.mdx now says 22 is the only line tested rather than the oldest, which implied a matrix we don't run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
Second docs-review pass, all four on text this branch introduced. The "run vitest against your own stack" recipe couldn't work as written. The harness signs its tokens with the E2E fixture's `sdk-dev-secret` (helpers.ts), while a default `make dev` server uses `change-me-in-production` — so setup's schema calls are denied and global setup burns its full 30s loop before dying on `schema not refreshed within 30s`, with nothing pointing at auth. The recipe now names `WH_CONFIG=tests/e2e/fixtures/config.yaml` and says why, including the fixture-only dedupe/DLQ/refresh settings several suites depend on. The CHANGELOG entry described the stale-server guard as refusing to start — the design from two commits earlier, which the commit before it replaced with kill-and-continue. It now matches the shipped behavior. `V=1` was described as streaming the subprocess log *instead of* capturing it; the orchestrator tees through io.MultiWriter, so the file is still written. What actually changes is that the on-failure tail dump is skipped. Both E2E test-file lists called helpers.test.ts a test of the harness's "wait/query helpers"; it covers waitForCondition only and never imports chQuery. Not fixed here: CONTRIBUTING.md:46's `configuration.md` → `.mdx`, which is real but pre-existing and already tracked in #444. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
…COVERAGE Code-review follow-up, both on the orchestrator. proc.Kill() returns os.ErrProcessDone when the leftover exits between pgrep and the kill — the realistic trigger being a re-run while a Ctrl-C'd run's server is still flushing coverage. That was treated as fatal, aborting the whole run and telling the reader to `kill -9` a PID that no longer exists: precisely the wedge the kill-instead-of-refuse change existed to avoid. The same file already handles this correctly when reaping the server it started. E2E_NO_COVERAGE was honored unconditionally, so an exported-and-forgotten var would let `make ci` write a green tmp/ci-passed-tree-<sha> push marker with the TS e2e report missing — test-e2e wipes tmp/coverage/ts-e2e first, ts-e2e's own threshold is informational, and ts-total then gates on ts-unit alone, yielding an `n/a` row that still passes. It is now ignored (with a log line) whenever COV_DEFER is set, which is exactly the targets that gate; standalone `make test-e2e` debugging is unaffected. The comment documented this hazard where enforcement was one condition away. Docs and CHANGELOG updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
…ording Third docs-review pass, both on text this branch added. "halving its cost" for E2E_NO_COVERAGE was asserted, not measured — and it does not survive arithmetic: the suite's wall clock is dominated by 23 visibility waits behind the 5s ingest linger plus cache.test.ts's explicit ttl sleep, roughly 60-115s of unavoidable waiting inside the 115.7-124.5s runs measured with coverage on. Halving would require v8 instrumentation to account for more than the entire non-waiting portion of the run. Replaced with what the flag actually does. A branch whose case rests on measured before/after should not carry a number nobody measured. clients/ts/README.md said "the oldest line this SDK is tested against" — the exact framing removed from sdk/index.mdx two commits ago for implying a matrix we do not run. .nvmrc pins 22 and setup-env consumes it, so 22 is the only line CI exercises; "oldest" tells an npm reader on 24 or 26 that their line is covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
Fourth docs-review pass. The recipe named the config but not the invocation, and the obvious thing to type — `WH_CONFIG=... make dev`, the idiom this same page uses three times for other overrides — is silently ignored: the dev recipe pins `WH_CONFIG=.config.local.yaml` inline, which beats anything inherited from the environment. A reader following it lands in exactly the misleading `schema not refreshed within 30s` failure the paragraph warns about. Now names `go run ./cmd/wavehouse`, the repo-root requirement (the fixture's policy.file_path is cwd-relative), and the `make dev` trap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
Found by actually running the recipe rather than reasoning about it. The command and the fixture-secret claim both check out — the harness's admin token gets 200 on /v1/schema/refresh and /v1/schema, and a token signed with the make dev secret gets 401, which is the failure the paragraph describes. What the run exposed is that the reader is told to point the *suite* at ClickHouse via CLICKHOUSE_URL, but the *server* needs to reach it too, and the fixture pins no clickhouse block — so it falls back to the config loader's localhost:9000 / 8123 defaults. Fine with `make deps-up`, silently wrong against a ClickHouse anywhere else. Names WH_CH_ADDR / WH_CH_HTTP_PORT for that case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
|
Warning Review limit reached
Next review available in: 20 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📜 Recent review details⏰ Context from checks skipped due to timeout. (5)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2026-08-12T05:38:52.277ZApplied to files:
🔇 Additional comments (3)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe SDK now requires Node 22. E2E polling enforces deadlines, propagates cancellation, applies request timeouts, and reports diagnostics. Orchestration cleans stale processes, supports local coverage controls, reports runtime versions, and updates Vitest path handling. ChangesE2E runtime and SDK support
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant E2ETest
participant waitForCondition
participant chQuery
participant ClickHouse
E2ETest->>waitForCondition: start condition polling
waitForCondition->>chQuery: poll with AbortSignal
chQuery->>ClickHouse: execute query with request timeout
ClickHouse-->>chQuery: response or timeout
chQuery-->>waitForCondition: result or diagnostic error
waitForCondition-->>E2ETest: success or timeout diagnostics
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📚 Docs preview is live → https://87a9af94-wavehouse-docs.wave-rf.workers.dev
|
Code Coverage OverviewLanguages: Go GoThe overall coverage in commit c85e410 in the Show a code coverage summary of the most impacted files.
Updated |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ca7331a7-9745-465e-b27b-1d38fcb17569
📒 Files selected for processing (20)
CHANGELOG.mdclients/ts/README.mdclients/ts/package.jsondocs/src/content/docs/development.mddocs/src/content/docs/sdk/index.mdxdocs/src/content/docs/sdk/queries.mddocs/src/content/docs/sdk/reference.mdpnpm-workspace.yamlscripts/orchestrator/main.gotests/e2e/sdk/batching.test.tstests/e2e/sdk/cache.test.tstests/e2e/sdk/dlq.test.tstests/e2e/sdk/helpers.test.tstests/e2e/sdk/helpers.tstests/e2e/sdk/ingest.test.tstests/e2e/sdk/ndjson.test.tstests/e2e/sdk/query.test.tstests/e2e/sdk/setup.tstests/e2e/sdk/stress.test.tstests/e2e/sdk/vitest.config.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
- GitHub Check: E2E tests
- GitHub Check: Coverage
- GitHub Check: Integration tests
- GitHub Check: Docs build
- GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (3)
clients/ts/README.md
📄 CodeRabbit inference engine (AGENTS.md)
Keep the TypeScript SDK README in sync with SDK-facing changes and public client behavior.
Files:
clients/ts/README.md
docs/src/content/docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
Documentation prose under the Starlight docs site must stay accurate against code, include runnable examples where relevant, and reflect code↔docs sync for changed behavior.
Files:
docs/src/content/docs/sdk/queries.mddocs/src/content/docs/sdk/index.mdxdocs/src/content/docs/sdk/reference.mddocs/src/content/docs/development.md
docs/src/content/docs/development.md
📄 CodeRabbit inference engine (AGENTS.md)
Update the development docs when changing build or test process details.
Files:
docs/src/content/docs/development.md
🧠 Learnings (18)
📚 Learning: 2026-07-07T16:19:29.374Z
Learnt from: CR
Repo: Wave-RF/WaveHouse PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-07-07T16:19:29.374Z
Learning: Applies to clients/ts/README.md : Keep the TypeScript SDK README in sync with SDK-facing changes and public client behavior.
Applied to files:
clients/ts/package.jsonclients/ts/README.mdpnpm-workspace.yamldocs/src/content/docs/sdk/index.mdxdocs/src/content/docs/sdk/reference.mdCHANGELOG.mdtests/e2e/sdk/setup.tsdocs/src/content/docs/development.md
📚 Learning: 2026-07-07T16:19:29.374Z
Learnt from: CR
Repo: Wave-RF/WaveHouse PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-07-07T16:19:29.374Z
Learning: Applies to clients/ts/**/*.{ts,tsx} : The TypeScript SDK in `clients/ts/` is the canonical client; when backend API changes affect users, update the SDK surface, auth handling, query builder, streaming helpers, pipes/policy helpers, or regenerated types as needed.
Applied to files:
clients/ts/README.mddocs/src/content/docs/sdk/index.mdxdocs/src/content/docs/sdk/reference.mdCHANGELOG.mddocs/src/content/docs/development.md
📚 Learning: 2026-08-11T21:55:41.895Z
Learnt from: jfwoods
Repo: Wave-RF/WaveHouse PR: 434
File: clients/go/go.mod:3-6
Timestamp: 2026-08-11T21:55:41.895Z
Learning: In the WaveHouse repository, `clients/go/go.mod` declares `go 1.24` as the deliberate minimum supported Go version for the published Go SDK. This SDK compatibility floor is independent of the server build toolchain declared by the root `go.mod` and referenced in `AGENTS.md`; do not require the Go SDK module to use the server toolchain version.
Applied to files:
clients/ts/README.mdpnpm-workspace.yaml
📚 Learning: 2026-06-10T15:01:09.027Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: docs/src/content/docs/development.md:0-0
Timestamp: 2026-06-10T15:01:09.027Z
Learning: In this repo’s Markdown review (all .md files), do not flag capitalization/style issues for literal paths starting with ".github/" (or any substring that is a path beginning with ".github/"). Treat ".github" as the correct lowercase dotfile directory name, even when it appears inside prose or code spans; automated checks such as LanguageTool’s "(GITHUB)" rule commonly produce false positives for this literal filesystem path.
Applied to files:
clients/ts/README.mddocs/src/content/docs/sdk/queries.mddocs/src/content/docs/sdk/reference.mdCHANGELOG.mddocs/src/content/docs/development.md
📚 Learning: 2026-07-07T16:19:29.374Z
Learnt from: CR
Repo: Wave-RF/WaveHouse PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-07-07T16:19:29.374Z
Learning: Applies to docs/src/content/docs/development.md : Update the development docs when changing build or test process details.
Applied to files:
docs/src/content/docs/sdk/reference.mddocs/src/content/docs/development.md
📚 Learning: 2026-06-26T12:23:26.034Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 346
File: internal/stream/subscriber_test.go:9-28
Timestamp: 2026-06-26T12:23:26.034Z
Learning: In this Go repository, the `**/*_test.go` table-driven test guideline is intended for genuinely multi-scenario tests. Single sequential behavioral-flow tests, such as `internal/stream/subscriber_test.go`'s `TestSubscriber_SendDeliversThenDropsWhenFull`, do not need to be rewritten into `[]struct{...}` + `t.Run(...)` when that would be artificial and less clear.
Applied to files:
docs/src/content/docs/sdk/reference.mddocs/src/content/docs/development.md
📚 Learning: 2026-06-10T19:54:03.032Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: CHANGELOG.md:0-0
Timestamp: 2026-06-10T19:54:03.032Z
Learning: In the Wave-RF/WaveHouse repository, CHANGELOG.md entries under `[Unreleased]` use descriptive Keep-a-Changelog leads (e.g. "The structured-query column allowlist is now a hard cap…"), NOT the Conventional Commit PR title verbatim. Do not flag CHANGELOG entry leads for not matching the PR title — that is not a rule in this repo. There is no `.coderabbit.yaml`, and neither `AGENTS.md` nor `CONTRIBUTING.md` requires CHANGELOG leads to match PR titles.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-07-07T16:19:29.374Z
Learnt from: CR
Repo: Wave-RF/WaveHouse PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-07-07T16:19:29.374Z
Learning: Applies to internal/stream/**/*.{go} : Streaming/SSE code must preserve the hub’s per-role projection model, subscriber queues, bucket fan-out, heartbeating, and metrics semantics.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-08-11T12:41:01.909Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 446
File: .github/actions/setup-env/action.yml:112-127
Timestamp: 2026-08-11T12:41:01.909Z
Learning: In WaveHouse CI, the shared `gomod-v1` cache in `.github/actions/setup-env/action.yml` can be saved by multiple Go jobs on an exact-key miss. Every workflow path that can write this cache must fully populate `~/go/pkg/mod` through the Makefile `go-mod-download` prerequisite before the post-job cache save. The `cov` target must retain this prerequisite.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-06-10T15:02:09.425Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: .github/workflows/ci.yml:232-237
Timestamp: 2026-06-10T15:02:09.425Z
Learning: In the Wave-RF/WaveHouse repository, `clickhouse/clickhouse-server:latest` is used deliberately in `tests/integration/setup_test.go`, `scripts/orchestrator/main.go`, and the CI workflow prefetch steps (`docker pull -q clickhouse/clickhouse-server:latest`). The `:latest` tag in the prefetch steps intentionally mirrors the tag testcontainers resolves at runtime — this is a deliberate canary approach. Pinning to a concrete version/digest is a separate decision tracked as a follow-up issue and should not be flagged as a supply-chain concern in CI workflow reviews for this repo.
Applied to files:
CHANGELOG.mddocs/src/content/docs/development.md
📚 Learning: 2026-08-11T21:56:03.599Z
Learnt from: jfwoods
Repo: Wave-RF/WaveHouse PR: 434
File: docs/src/content/docs/sdk/go/queries.md:356-362
Timestamp: 2026-08-11T21:56:03.599Z
Learning: In Wave-RF/WaveHouse Go SDK cursor pagination, `fetchNextTyped` uses only the first `QueryBuilder.OrderBy` column and a strict `gt` or `lt` filter. Duplicate values at a page boundary can skip rows. The Go SDK documentation must require a unique ordering column until the shared Go and TypeScript composite-cursor or tie-breaker implementation tracked in GitHub issue `#452` is available.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-06-26T15:07:28.749Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 0
File: :0-0
Timestamp: 2026-06-26T15:07:28.749Z
Learning: In the Go SSE implementation in `internal/api/stream.go`, keepalive frames from `internal/stream.Heartbeater` are only written from the post-replay select loop. The replay/gap-fill step is synchronous before entering that loop, so registering the `internal/stream.Subscriber` before replay does not materially improve idle-time coverage during replay; it can at most buffer one heartbeat in the subscriber's capacity-1 queue. Covering a genuinely long replay would require interleaving replay with the select loop and is tied to the broader delivery-path rework tracked by Issue `#294`.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-08-11T21:56:00.508Z
Learnt from: jfwoods
Repo: Wave-RF/WaveHouse PR: 434
File: clients/go/query_builder.go:278-291
Timestamp: 2026-08-11T21:56:00.508Z
Learning: In `clients/go/query_builder.go`, `fetchNextTyped` intentionally treats a failed JSON decode of a non-object typed `Row` as normal end-of-pagination. This behavior matches the existing “cursor column was not in the projection” path and TypeScript SDK parity. The broader behavior change is tracked in GitHub issue `#452`.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-07-07T16:19:29.374Z
Learnt from: CR
Repo: Wave-RF/WaveHouse PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-07-07T16:19:29.374Z
Learning: Applies to internal/ingest/types.go : `EventMessage` JSON tags and ingest event shape must stay aligned with docs, SSE examples, and ClickHouse INSERT column order.
Applied to files:
CHANGELOG.md
📚 Learning: 2026-05-19T14:41:38.228Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 142
File: docs/scripts/screenshot.mjs:31-31
Timestamp: 2026-05-19T14:41:38.228Z
Learning: `docs/scripts/screenshot.mjs` in the Wave-RF/WaveHouse repo is an intentionally manual dev-iteration tool (not wired into CI). Response-status validation and retry logic are deliberately deferred until the script is promoted to a CI visual-regression workflow. Do not flag the absence of `response.ok()` checks as an issue in this file.
Applied to files:
tests/e2e/sdk/setup.tsdocs/src/content/docs/development.md
📚 Learning: 2026-05-13T20:41:09.256Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 125
File: internal/api/health_test.go:100-163
Timestamp: 2026-05-13T20:41:09.256Z
Learning: In `internal/api/health_test.go` (WaveHouse), every handler test explicitly asserts `Content-Type: application/json` and `X-Content-Type-Options: nosniff` headers, including on 503 responses. This is deliberate regression coverage: the comment in `TestHealth_Readiness_PingFails` explains that without the 503-path header test, a future refactor moving header setup into the success branch would silently drop headers on error responses. New boot-degraded tests should follow the same pattern.
Applied to files:
docs/src/content/docs/development.md
📚 Learning: 2026-07-07T12:38:12.052Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 378
File: internal/auth/auth.go:119-132
Timestamp: 2026-07-07T12:38:12.052Z
Learning: In this repo, do not add or recommend logging/tracing client IP addresses using naive or untrusted sources (e.g., `r.RemoteAddr` or directly trusting/deriving `X-Forwarded-For`) anywhere in the Go codebase. `middleware.RealIP` was removed due to IP-spoofing risks, and proper trusted-proxy-aware client-IP handling is intentionally deferred to issue `#333`. During code review, if proposed changes would record client IPs (including in audit paths such as `internal/auth/auth.go`), reject/redirect until `#333` lands with correct trusted-proxy configuration and safeguards.
Applied to files:
scripts/orchestrator/main.go
📚 Learning: 2026-08-11T21:55:53.726Z
Learnt from: jfwoods
Repo: Wave-RF/WaveHouse PR: 434
File: clients/go/stream_test.go:159-176
Timestamp: 2026-08-11T21:55:53.726Z
Learning: For Go files in this repository, do not report direct type assertions solely because the `forcetypeassert` rule is commented out in `.golangci.yml`. Only flag a type assertion when there is an independent correctness, safety, or maintainability issue.
Applied to files:
scripts/orchestrator/main.go
🪛 LanguageTool
docs/src/content/docs/development.md
[style] ~375-~375: Since ownership is already implied, this phrasing may be redundant.
Context: .../`. The orchestrator always provisions its own stack — a fresh ClickHouse testcontaine...
(PRP_OWN)
🪛 OpenGrep (1.26.0)
tests/e2e/sdk/helpers.test.ts
[ERROR] 89-89: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🔇 Additional comments (19)
clients/ts/package.json (1)
26-26: LGTM!docs/src/content/docs/sdk/queries.md (1)
65-65: LGTM!tests/e2e/sdk/helpers.ts (3)
76-87: LGTM!
103-150: LGTM!
185-201: 🩺 Stability & AvailabilityKeep
headers: { connection: "close" }. Node’s Undici implementation accepts and emits this header; the Fetch forbidden-header rule does not apply here.> Likely an incorrect or invalid review comment.tests/e2e/sdk/helpers.test.ts (1)
15-125: LGTM!tests/e2e/sdk/batching.test.ts (1)
36-39: LGTM!Also applies to: 65-68, 100-122
tests/e2e/sdk/cache.test.ts (1)
53-56: LGTM!tests/e2e/sdk/dlq.test.ts (1)
50-53: LGTM!tests/e2e/sdk/ndjson.test.ts (1)
27-30: LGTM!Also applies to: 65-68, 96-99, 127-130, 159-162
tests/e2e/sdk/query.test.ts (1)
34-37: LGTM!tests/e2e/sdk/stress.test.ts (1)
43-46: LGTM!tests/e2e/sdk/ingest.test.ts (1)
33-37: LGTM!Also applies to: 60-63, 111-115, 147-151, 198-201, 254-257, 340-343
CHANGELOG.md (1)
26-27: LGTM!Also applies to: 56-57, 112-112
scripts/orchestrator/main.go (1)
42-42: LGTM!Also applies to: 70-110, 246-264, 319-338
tests/e2e/sdk/setup.ts (1)
15-16: LGTM!Also applies to: 101-132
tests/e2e/sdk/vitest.config.ts (1)
8-9: LGTM!Also applies to: 20-22, 45-47
docs/src/content/docs/development.md (1)
363-363: LGTM!Also applies to: 375-397
docs/src/content/docs/sdk/reference.md (1)
156-156: LGTM!
Both from CodeRabbit review on #455, both verified against Node 22 before acting. chQuery's catch rewrote any error as a timeout whenever `ceiling.aborted` was set. The ceiling keeps running after fetch settles, so a `!res.ok` throw or a JSON.parse failure on a slow-but-successful response could land there with the flag already true — discarding ClickHouse's own error text, which is the opposite of what the reclassification is for. It now requires the caught error to actually be an abort. Measured: fetch rejects with TimeoutError for an AbortSignal.timeout and AbortError for a controller abort, so the name is a sound discriminator. E2E_CH_QUERY_TIMEOUT_MS went to AbortSignal.timeout unvalidated. Exported empty it becomes 0 and aborts every query on the next tick; typo'd it becomes NaN, which throws a RangeError naming neither the variable nor the value. Both verified on node 22 (0 is accepted, NaN/fractions/negatives/ out-of-range all RangeError). Now rejected up front with a message that names both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
The comment claimed AbortSignal.timeout rejects anything outside the signed 32-bit range. It doesn't: measured on Node 22.23.2 and 26.7.0, it accepts any integer in [0, 4294967295], and values above 2^31-1 overflow setTimeout's int32 so Node silently clamps the delay to 1ms with a TimeoutOverflowWarning — an instant abort on every query, which is the failure class this validation exists to prevent. So the bound is right and the reason was wrong, which is the worse of the two mistakes: a reader who checked the claim would find it false and might widen the bound to 4294967295 "to match the API", reintroducing the 1ms clamp. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJvon2pyU4zaSHTGJj8fE
# Conflicts: # CHANGELOG.md
Closes #269. Closes #464. Adds the SDK's HTTP-customization surface — `options.headers`, `options.fetchOptions`, and `options.fetch` — so a WaveHouse behind a gate is reachable from the client. ## Why `ClientConfig` exposed only `baseURL`, `auth`, and `options.maxRetries`. A WaveHouse fronted by a header-gated proxy (Cloudflare Access, an mTLS sidecar, an auth gateway) simply couldn't be talked to — a defense-in-depth gate forced consumers off the SDK entirely. Found via WaveHouse-Stats dogfooding. The second driver is a runtime bug consumers can't fix from inside the SDK: undici 8.8.0–8.9.0 stalls a request before it goes out when a keep-alive socket is reused on an idle event loop ([nodejs/undici#5600](nodejs/undici#5600), fixed in 8.10.0), and Node 26 bundles 8.9.0. Severity varies with runtime and idle gap — upstream measured ~450–465ms; on Node 26.7.0 with 6s gaps against a stub server answering instantly: ``` SDK max=23990ms >500ms: 4/5 [12, 23990, 2203, 2100, 2099] ``` ## What landed | Option | Purpose | |---|---| | `options.headers` | Static headers on every REST request — the CF-Access case | | `options.fetchOptions` | Extra `RequestInit` merged in — `cache`, `keepalive`, `credentials`, Next.js `next: { tags }` | | `options.fetch` | Replace the HTTP implementation outright | Shaped after Supabase (`global.fetch`/`global.headers`) and OpenAI/Anthropic (`fetch`/`fetchOptions`/`defaultHeaders`) rather than invented here. **Header precedence**, lowest to highest: `options.headers` → `auth` → SDK-computed (`Content-Type`, `Accept`). Names match case-insensitively, and a collision **drops** the configured value rather than joining it. Both rules come from bugs other SDKs shipped: a global `Content-Type` joined with an upload's own produced `application/json, image/png` and 415s, and a case-sensitive `Authorization` check let a lowercase spelling ride alongside the canonical one. Two configured spellings of one header collapse to the last, so `Headers` can't comma-join them on the wire. **`fetchOptions` can't corrupt the request** — `method`, `headers`, `body`, and `signal` are applied *after* the spread. `fetchOptions.headers` is ignored rather than merged; `options.headers` is the header channel, and merging both would give one concept two precedence stories and a side door around `auth`. ## The undici workaround took three tries to get right Worth reading before you follow it elsewhere, because the obvious forms don't work. Measured on undici 8.9.0, 10ms server, 1.5s idle gaps (per-request ms): ``` default Agent() 22 1513 1493 584 Agent({ keepAliveTimeout: 1_000 }) 13 1500 1496 1513 ← inert Agent({ pipelining: 0 }) 13 13 12 12 same script on undici 8.10.0 22 15 13 13 ``` Lowering `keepAliveTimeout` does nothing — the retirement timer is starved by the same idle event loop that causes the bug. Worse, **importing a fixed undici isn't enough on its own.** undici keeps its connection pool on a shared `globalThis` symbol claimed by whichever copy loads first — the bundled one, on an affected runtime. With 8.9.0 loaded first and 8.10.0's `fetch` doing the work: ``` no explicit dispatcher 21 1514 1495 583 ← still stalling explicit new Agent() 17 14 12 13 ``` So the documented snippet passes `dispatcher` explicitly. Verified end-to-end through the SDK with 8.9.0 loaded first: `24, 15, 13, 14ms`. ## Breaking changes **`PipeRef.fetch` no longer accepts `limit`** — closes #464, raised by CodeRabbit here. It took the shared per-call options type, which carries `limit`, but forwarded only `signal`, so `wh.pipe('x').fetch({ limit: 10 })` compiled and silently did nothing. Nothing to forward: the endpoint binds the body as the pipe's *parameters* (`internal/api/pipes.go` → `pipes.BindParams`), so a row cap belongs in the pipe's SQL as `{{limit}}`, passed via `wh.pipe(name, { limit })` — which is what the docs already showed. Now a dedicated exported `PipeRequestOptions` — `signal?: AbortSignal; limit?: never`. The `never` is load-bearing. The first attempt used `Pick<RequestOptions, "signal">`, which CodeRabbit correctly flagged as only half a fix: TypeScript's excess-property check is a freshness heuristic, so it rejects a fresh literal but not a *variable*. `const opts: RequestOptions = { signal, limit: 10 }; wh.pipe('x').fetch(opts)` still compiled and still dropped the limit — the original defect, in the shape real code is more likely to take. Both forms are now pinned by `@ts-expect-error` tests. **Collateral effect, and the half you'll actually hit:** a value *declared* `RequestOptions` no longer assigns to a pipe `.fetch()` at all, even carrying no limit at runtime, since the declared type permits one. Type a shared options object as `PipeRequestOptions` — the table and query-builder `.fetch()` accept it too, so it works everywhere — or inline `{ signal }`. Structural wrappers are unaffected: method parameters compare bivariantly, so `interface Fetchable { fetch(opts?: RequestOptions): … }` is still satisfied by `PipeRef` (verified). Pre-existing on main; folded in here because this PR renames that exact type and gives it a JSDoc describing it as the options for `.fetch()`, which made the false advertisement more prominent rather than less. **`FetchOptions` → `RequestOptions`** (the per-call type accepted by `.fetch()`), no deprecated alias. The old name collided conceptually with the new `options.fetchOptions`, which — per the ecosystem — means "extra `RequestInit`", not "options for our `.fetch()` method". Nothing consumes it pre-1.0; renaming the import is the whole migration *for this one* — the `PipeRef.fetch` narrowing above is a separate, behavioural break in the same file. The module-private `RequestOptions` in `http.ts` became `RequestSpec` to free the name. ## Design notes `fetch` stays optional all the way to the internal `HttpContext` rather than being resolved at construction, so the default path calls the global directly. That keeps it **late-bound** (replacing `globalThis.fetch` after a client exists still works — what `vi.stubGlobal` does) and avoids invoking a **detached** `fetch` reference, which throws "Illegal invocation" on browsers, workerd, and Bun. Both are pinned by tests. Supabase's `resolveFetch` independently converges on the same closure-per-call shape; the SDKs that capture at construction are the ones with stale-fetch bug reports. `FetchLike` is the standard `fetch` signature, written out rather than as `typeof fetch` because that resolves differently depending on whether the consumer's `lib` includes DOM. ## Scope REST only. `.stream()` and `.liveQuery()`'s live connection go through `EventSource`, which accepts neither headers nor a `fetch` — so a header-gated deployment can query but not stream until #203 changes that transport. Called out in a `:::caution` on the docs page rather than a footnote: the equivalent gap in Supabase's realtime client was found by a user whose RLS policies silently stopped matching. `.liveQuery()`'s initial backfill is an ordinary REST call and *is* covered. Browser callers should know custom headers must also pass CORS preflight, and WaveHouse allow-lists a fixed set with no config knob — documented. ## Tests 158 pass, 1 skipped across the package (30 in `client.test.ts`), covering: routing through a supplied fetch with the full `RequestInit` asserted; global fallback; late-binding after construction; retries using the override; headers applied to every request; `Content-Type` not displaceable; `auth` beating a lowercase `authorization`; two casings collapsing to one; `fetchOptions` merged; `fetchOptions` unable to touch `method`/`body`/`headers`; and two `@ts-expect-error` pins that `PipeRef.fetch` rejects `limit`, as a literal and via a named `RequestOptions` value. Beyond unit tests, every documented snippet was compiled against the **built** package under DOM-inclusive and Node-only `lib` configs, and the header behaviour was verified **on the wire** against a live server — the CF-Access header sent, `auth` beating an impostor, `Content-Type` intact. That last step matters here: compile-plus-happy-path is exactly the verification that let the implied-dispatcher bug through twice. ## Related - #459 — per-call `headers`/`fetchOptions`/`fetch` and dynamic header callbacks, deliberately deferred - #458 — the `auth()` per-request contract, split out - #203 — SSE transport; owns whether streaming ever gets these - #455 / #440 — the e2e-harness fix for the same undici bug (`connection: close`, right for a test helper, wrong for a production client — hence this escape hatch) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #440.
make cihad been failing locally ~3 runs in 5 while CI stayed green. #440 concluded that was environmental — CPU contention on one machine, no repo change would have prevented it. That conclusion was wrong, and this PR fixes the real cause. The issue has been corrected.Two defects, the first hiding the second
waitForConditioncouldn't enforce its budget. It checked the clock only on loop entry, so one slowfn()overran without bound — a 10s budget was measured running 28s, past the caller'stestTimeout. Vitest then killed the test first and reported a timeout naming neither the condition nor how long the poll actually waited. It now racesfn()against the deadline, aborts the in-flight call through anAbortSignalhanded tofn, and reports poll shape:N poll(s), slowest Xms.That last part is what made the second defect visible. Many fast polls means the write never landed; a few slow ones means the polling itself was starved. They had been indistinguishable.
chQueryinherited idle pooled connections. undici 8.8.0–8.9.0 stalls for seconds before writing a request onto a socket that has been idle a few seconds — nodejs/undici#5600, a scheduling regression inscheduleIdleSocketValidation(), fixed in 8.10.0. This suite has multi-second idle gaps by construction: the 5s ingest linger sits between every write and the first poll of its visibility wait, so every visibility wait sat in the triggering window.Bisected with Node held constant at 22, varying only undici:
Why CI never saw it
.nvmrcpins Node 22 andsetup-envconsumes it vianode-version-file, so CI runs undici 6.28.0. Node 26 bundles 8.9.0. Locally.nvmrcis inert without a version manager andengines: ">=22"is a floor that 26 satisfies, so the drift was silent.Result
make test-e2eFaster as well as green — removing the stalls shortens every run, which is the mechanism's own prediction rather than just an absence of failures.
Ruled out by measurement, not reasoning
ClickHouse (p99 ≤ 2.5 ms measured during live failing runs, on the real tables with the real query shapes), Docker/OrbStack (reproduces against a bare host-loopback server with no container in the path), DNS, the libuv threadpool, GC, event-loop blocking, machine load, and CPU contention.
Also in here
.nvmrc. This is the cheap fix for the whole class: a version-specific failure was previously indistinguishable from a code failure, which is what turned a transport bug into days of investigation.wavehouse-covbefore starting. A killed run leaves one, and it corrupts the next run through the sharedtmp/dataand log file — presenting as a dozen unrelated tests failing to see their rows, in a log blaming a container that no longer exists. It kills and continues (refusing would wedge a shared runner), tolerating a process that exits on its own in the meantime.E2E_NO_COVERAGE=1for local debugging, fenced so it can't green a coverage gate by omission whenCOV_DEFERis set.@wavehouse/sdkengines.node>=18→>=22. Nothing ever tested 18, and 18/20 are both past upstream EOL. Consumers on <22 now getEBADENGINE(npm) or a hard failure (pnpm withengine-strict), so the README and docs state the requirement — the README previously stated none.batching's visibility wait had ~700ms of headroom over the 5s linger where every other wait allows 10s. Widened; the>= 4500mslower bound that carries the test's meaning is unchanged.vitest.config.ts:__dirname→import.meta.dirname, silencing the Vite 8configLoader: 'native'warning.development.mddescribed an E2E harness that doesn't exist (asetup.tsthat "probes ports before starting Docker services" — it probes, then throws; and amake devdetection that reuses a healthy:8080— the orchestrator always provisions its own stack). Corrected, with the env knobs documented and a working recipe for running vitest against a stack you manage yourself.Verification
make cigreen on both Node 22 and Node 26, so the workaround path and CI's path are each exercised. Flake rate measured over 5-run series before and after, at normal process priority, with an orphan guard between runs.Follow-ups, not in scope
defaultMaxWait = 5s(internal/ingest/worker.go:56, standingTODO) is what creates the idle gaps and burns most of the suite's runtime. Making it configurable would shorten the suite and widen every margin.tests/e2e/sdkis not typechecked bymake verify—typecheck-tscovers only@wavehouse/sdk, so these helper changes would not have been caught by CI.