Skip to content

test(#630): freeze native Fetch/Response/cancellation semantics (phase 1) - #640

Merged
BorisTyshkevich merged 4 commits into
mainfrom
feat/630-p1-transport-characterization
Aug 7, 2026
Merged

test(#630): freeze native Fetch/Response/cancellation semantics (phase 1)#640
BorisTyshkevich merged 4 commits into
mainfrom
feat/630-p1-transport-characterization

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Part of #630 — replacing the rejected @clickhouse/client-web adoption path (#585) with
a first-party, Fetch-native @altinity/clickhouse-http package extracted from generic
protocol code SQL Browser already maintains.

This is Phase 1 of 8: a characterization/test-infrastructure-only unit. It does not
create the package, refactor transport, or touch any production source. It strengthens
the existing transport contract and adds a real-browser (Chromium/WebKit) fault harness
proving the invariants that phases 2+ will freeze behind the new package boundary.

Claims: A1 (native Fetch/Response/cancellation contract characterized in unit and
Chromium/WebKit tests), A3 (low-level request preserves native Response identity, exact
SQL/Auth, raw bytes, one Fetch call, caller-signal lifetime). A2 and A4–A18 remain
deferred to later phases.

Approved plan: see the ship-log comment on #630 for the plan-review conversation link
(ChatGPT-author / Fable-high-approve loop, approved pass 3/5).

Contract coverage

All 14 characterization bullets from the issue's Phase 1 section are covered — see the
invariant map below. Every one of the 9 hard invariants this phase characterizes has an
enforcement mechanism, a test/compile-time proof, and a sabotage case that fails as
expected (verified independently twice — once by the implementer, once by a separate
high-effort pre-PR review — with no sabotage surviving in either pass).

# Hard invariant Enforcement (production) Proof Sabotage
1 Native Response identity send() returns the fetch Response directly clickhouse-transport-contract.ts identity asserts (2xx/non-2xx) + e2e Scenario 1/2 page-realm === .clone()/rewrap → identity asserts fail
2 No hidden body consumption send() never reads the body bodyUsed === false pre-consumption asserts early .text() → asserts fail
3 One Fetch invocation one deps.fetch()(...) call, no retry unit call-count asserts + e2e wrapper count duplicate call → count≠1 fails
4 Exact SQL body: request.sql verbatim pathological-literal exact-match (unit + server) .trim()/rewrite → exactness fails
5 Opaque Authorization complete string copied, no cache/parse Bearer/Basic/custom matrix across sends scheme-normalize → matrix fails
6 Live origin()/fetch() accessors both called fresh per send() accessor-swap-between-sends asserts snapshot-at-construction → asserts fail
7 Native cancellation lifetime caller's AbortSignal passed directly to native fetch signal-identity unit test + e2e Scenarios 3–7 omit signal → all fail
8 Cancellation isolation no shared transport cancellation state e2e Scenario 7 (shared transport, independent A/B) shared controller reuse → B disrupted
9 Raw-byte safety send() never decodes/re-encodes invalid-UTF-8 byte-identical unit + e2e .text()+reconstruct → byte proof fails

Tests

  • npm run check:types && npm run check:arch && npm run check:schemas && npm run check:examples && npm test && npm run build — green.
  • npx vitest run tests/unit/clickhouse-http-transport.test.ts --config tests/vitest.config.ts — green.
  • npm run test:client-spike — 116 passed, 19 skipped, 0 failed.
  • npm run test:e2e -- tests/e2e/clickhouse-http-transport.spec.js --project=chromium --project=webkit — 18/18 passed.
  • Root e2e regression, --project=chromium --project=webkit — 432 passed, 4 skipped (pre-existing, unrelated).
  • Firefox: explicitly skipped for the new spec (test.skip(({browserName}) => browserName === 'firefox', ...)) per repo policy (Firefox can't launch locally; CI supplies it for the full nightly/manual matrix, not this PR's Chromium-only e2e job).

Build

npm run build succeeds — single-file dist/sql.html (2,126,288 bytes), no new runtime dependency.

Invariant/sabotage verification

Independently confirmed twice: once during implementation (all 9 sabotage mutations
caught, then reverted from saved bytes), and once by a separate high-effort read-only
review over the complete branch diff, which additionally re-ran 4 of the sabotage
mutations itself (shared-controller cancellation bug, swallowed read-rejection in
streamLines, chUrl ordering swap, Response.clone()) — all caught, none survived.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: this unit touches only tests/** and one CI path-filter line — zero production source changed
  • No new runtime dependency
  • CHANGELOG.md ([Unreleased]) updated
  • Reconciled affected tracked work — ship-log comment on Epic: extract the Fetch-native ClickHouse client into a reusable package #630 updated; the issue's own ## Phases checklist is ticked only after merge is verified on origin/main, per its explicit stated convention

Part of #630.

BorisTyshkevich and others added 3 commits August 7, 2026 12:23
…hase 1

Characterization-only unit for issue #630 Phase 1: strengthens the existing
ClickHouseTransport contract/implementation unit suites (strict Response
identity, exact pre-abort Fetch count, pathological SQL/Authorization
matrix, invalid-UTF-8 byte safety, live fetch()/origin() accessors, exact
zero/empty/reserved-value URL serialization) and adds a real Chromium/WebKit
browser proof (tests/e2e/clickhouse-http-transport.{html,spec.js}) driving
the actual createHttpTransport against a real cross-origin fault server for
native cancellation-lifetime scenarios (pre-abort, awaiting-header abort,
post-header pending-read abort, streamLines() callback shutdown, concurrent
A/B isolation, abort-after-completion).

fault-server.mjs gains byte-safe request-body capture (Buffer.concat instead
of per-chunk string concatenation), an opt-in browser/CORS mode, and a
deterministic post-header-abort-hold fixture — all additive, existing
callers unaffected. The CI e2e path filter now tracks this one shared
fixture file since the new root spec depends on it.

No production transport/auth/consumer file is touched — this phase only
characterizes and strengthens proof around the existing boundary.
Author-side readiness review (per per-issue-cycle.md step 3) found the
opt-in CORS writeHead wrapper added in the previous commit assumed Node's
2-arg writeHead(status, headers) call shape; Node's real signature also
allows writeHead(status, statusMessage, headers), which the wrapper would
have silently mishandled (spreading a string into object keys) had any
fixture used it. No current fixture does, so this was latent, not an active
bug — hardened defensively since it's shared test infrastructure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

ChatGPT review pass 1

Reviewed head: 6a3941324ba03724863061ed8847e2b5aa809983

Verdict: REVISE

Findings

  1. P1 — hard invariant 5 does not actually prove the required custom Authorization scheme case. tests/unit/clickhouse-transport-contract.ts names its matrix “Bearer, Basic, and custom-scheme”, but the values are Bearer, Basic, Digest, then Bearer again. Digest is a standard HTTP authentication scheme, not a custom/nonstandard scheme. The e2e path also exercises Bearer only. A future implementation that preserves the known standard schemes but parses/rejects/normalizes unknown schemes could therefore pass the committed proof, contrary to the Phase-1 requirement that Authorization be opaque and to the stated Bearer/Basic/custom matrix. Add at least one genuinely nonstandard scheme/value (for example CustomAuth opaque-value) and verify a sabotage that mishandles unknown schemes makes the proof fail.

  2. P1 — the shared fault server changes legacy no-option error semantics despite the additive/backward-compatible requirement. In tests/spike/clickhouse-client/fault-server.mjs:71, res.on('error', () => {}) is installed unconditionally, before if (cors). The function comment says cors defaults off so pre-existing callers keep their behavior, but parity.test.ts / run-matrix.mjs callers that use startFaultServer() now silently swallow every ServerResponse error. That can mask a fixture/server failure that previously surfaced, and it is unrelated to legacy callers. Scope the suppression to the opt-in CORS/cancellation mode, or ignore only the specific expected aborted-socket error for the new cancellation fixtures, leaving startFaultServer() default behavior unchanged.

  3. P3 — strict textual acceptance only: .github/workflows/ci.yml adds the correct single exact path rule (tests/spike/clickhouse-client/fault-server.mjs) and does not broaden it to tests/spike/**, but the hunk adds six explanatory comment lines as well. If “the e2e path filter gains exactly one line” is meant literally as one physical added line, remove/move those comments. If it means exactly one filter entry, this requirement is satisfied. This is not the reason for the REVISE verdict.

Verified

  • Complete base→head compare contains exactly 7 changed files and no src/** files. The three protected production files have identical blob SHAs at base 08a16ded60d55be5ca0dbccfe1891511e73cf8ea and reviewed head: clickhouse-http-transport.ts = 475719d05b7358c99f007e016b853289feea518d, clickhouse-transport.types.ts = 098270200fc194bf9cd79965fedca149c9b22b23, ch-client.ts = 61f27e97f9268b1784a2ec2fc2d2ecadfe17200a.
  • The new browser harness imports the real production createHttpTransport; build/e2e-serve.mjs resolves /src/net/clickhouse-http-transport.js to the real .ts source and type-strips it rather than substituting a mock implementation.
  • Scenario 6 is non-vacuous: the fixture holds the second write for 3000 ms and the test waits past that hold after rejection while checking callback counts remain frozen. Scenario 7 uses one shared transport, independent A/B signals, and pending second reads before aborting A, so shared cancellation state would disrupt B and fail.
  • Exact SQL and URL expectations are independently authored literals rather than computed with chUrl(); the Firefox skip is explicit.
  • GitHub CI for the exact head’s merge ref succeeded. The test job ran check:schemas, check:examples, check:arch, check:types, 7162 unit tests with 100/97.14/100/100 coverage, and build; the PR e2e job ran Chromium and passed 218 tests.

Verification limitation

This runtime could not resolve github.com from git, so I could not create a local clone or independently rerun the caller-reported WebKit/sabotage runs. I used the authenticated GitHub connector for the canonical full diff/files/history and the exact-head GitHub Actions logs. The caller-referenced local plan path /Users/altinity/tmp/plan-630p1.md is not mounted here, so I reviewed against the acceptance subset supplied in the review request.

- Strengthen invariant 5's Authorization matrix with a genuinely
  nonstandard scheme (XAuth) alongside Bearer/Basic/Digest, so a
  transport that special-cases a closed allowlist of known schemes
  can no longer pass a matrix built only from IANA-registered schemes.
- Scope fault-server.mjs's ServerResponse error-suppression handler to
  the opt-in cors:true path only, matching its own docstring that
  cors defaults off and every pre-existing no-option caller keeps
  today's behavior. Add getLastErrorListenerCount() introspection and
  a parity.test.ts regression pair proving the scoping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz

Copy link
Copy Markdown
Collaborator Author

ChatGPT review pass 2

Previously reviewed head: 6a3941324ba03724863061ed8847e2b5aa809983

Reviewed head: bebe53add1ddeae4d681f64dc7a59e8f0768d294

Verdict: SHIP

Pass-1 finding reassessment

  1. Resolved — opaque Authorization custom-scheme proof. The fix adds XAuth opaque-value-1 to the sequential Authorization matrix alongside Bearer/Basic/Digest. XAuth is genuinely nonstandard, so a transport that only preserves a closed allowlist of known authentication schemes can no longer pass the proof. The assertion still checks the complete header value verbatim on every send, including a later different Bearer value to guard against caching.

  2. Resolved — legacy fault-server error semantics. res.on('error', () => {}) is now inside the opt-in if (cors) branch. Default startFaultServer() callers no longer receive that suppression. The new parity regression pair checks zero added ServerResponse error listeners for the legacy no-option path and one for cors: true. The additional getLastErrorListenerCount() surface is test-only, read-only observability and does not change request behavior.

  3. Withdrawn per clarified acceptance criterion — CI path filter. I accept the rebuttal: the requirement is exactly one new filter entry, not one physical diff line. The updated PR adds only tests/spike/clickhouse-client/fault-server.mjs to changes.e2e; it does not add a broader tests/spike/** pattern.

Complete updated-PR regression scan

  • The old→new delta is one commit and touches only fault-server.mjs, parity.test.ts, and clickhouse-transport-contract.ts; the complete PR now has 8 changed files, all test/docs/CI files.
  • The three protected production files remain byte-identical to current main by Git blob SHA: clickhouse-http-transport.ts = 475719d05b7358c99f007e016b853289feea518d, clickhouse-transport.types.ts = 098270200fc194bf9cd79965fedca149c9b22b23, ch-client.ts = 61f27e97f9268b1784a2ec2fc2d2ecadfe17200a.
  • Authorization redaction in the shared fault server is unchanged: raw authorization is filtered out and only scheme/presence are logged.
  • The browser harness still imports the real production createHttpTransport; the e2e static server resolves the .js import to the real .ts source and type-strips it. The wrapper only counts/captures/delegates native Fetch calls.
  • Scenario 6 remains non-vacuous: cancellation occurs from the first real onChunk, streamLines() rejects with AbortError, and callback counts are checked again after waiting beyond the 3000 ms held second write. Scenario 7 still uses one shared transport, independent A/B signals, and both second reads are pending before A is aborted; B must receive the held chunk and complete cleanly.
  • Exact SQL and URL expectations remain independently authored rather than derived through chUrl() for their expected values. The new e2e spec still explicitly skips Firefox.
  • Exact-head GitHub Actions is green: the test job ran schemas/examples/architecture/types, 7162 tests at 100/97.14/100/100 coverage, and build; the PR e2e job ran Chromium and passed all 218 tests.

Verification limitation

This runtime still cannot independently clone the repository or rerun the caller-reported local WebKit/sabotage suite. I reviewed the canonical GitHub full diff and history, the exact old→new delta, current file contents/blob identities, and the exact-head GitHub Actions run. I found no remaining actionable finding or regression in the updated PR.

@BorisTyshkevich
BorisTyshkevich merged commit 20f2b59 into main Aug 7, 2026
8 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/630-p1-transport-characterization branch August 7, 2026 11:31
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.

1 participant