Skip to content

test(driver): branch coverage >=80% on src/driver/{http2,undici}#121

Merged
productdevbook merged 2 commits into
mainfrom
test/driver-coverage
May 3, 2026
Merged

test(driver): branch coverage >=80% on src/driver/{http2,undici}#121
productdevbook merged 2 commits into
mainfrom
test/driver-coverage

Conversation

@productdevbook
Copy link
Copy Markdown
Owner

Summary

Drives previously-uncovered branches in src/driver/http2.ts and src/driver/undici.ts so each file lands at >=80% branch coverage and the src/driver aggregate clears the same bar. No source changes — tests only.

Coverage (before -> after, branches)

File Before After
src/driver/http2.ts 60.37% 81.13%
src/driver/undici.ts 70.00% 80.00%
src/driver/ overall 64.38% 80.82%

Measured via pnpm vitest run --coverage --coverage.include='src/driver/**'.

Scenarios added

http2.ts:

  • host header stripped before sending (server sees only :authority)
  • multi-value response header append (set-cookie array path)
  • server-initiated RST_STREAM rejects via the error listener
  • pre-aborted signal rejects synchronously (queueMicrotask path)
  • cached session reuse across sequential requests (bumpIdle on hit)
  • recovery when the cached session was destroyed between calls
  • idle-timer expiry closes the session, next call opens a fresh one
  • dispose() is idempotent and a no-op before any request

undici.ts:

  • POST forwards body bytes and sets duplex: 'half'
  • bodyless GET omits both body and duplex
  • request.signal forwards through to the dispatcher
  • dispatcher object identity preserved (same reference, not cloned)
  • POST/PUT/DELETE method propagation
  • request.redirect propagation
  • ensureFetch caches the import across concurrent requests
  • real-undici end-to-end POST (covers dynamic-import branch)
  • real-undici non-2xx surfaces as HTTPError
  • error path when undici exposes no fetch

Test plan

  • pnpm vitest run test/driver-undici.test.ts test/driver-http2.test.ts — 30/30 pass
  • pnpm vitest run — full suite 932/932 pass (twice in a row)
  • pnpm lint — no new errors introduced
  • pnpm fmt — formatted clean
  • http2 sockets destroyed manually on teardown (per 1ba17a7 pattern)

🤖 Generated with Claude Code

Drives the previously-uncovered branches in `src/driver/http2.ts` and
`src/driver/undici.ts` so each file lands at >=80% branch coverage and
the `src/driver` aggregate clears the same bar.

http2.ts: 60.37% -> 81.13% branches.
undici.ts: 70% -> 80% branches.
src/driver overall: 64.38% -> 80.82% branches.

http2 scenarios added:
- host header stripped before sending (server sees only :authority)
- multi-value response headers append (set-cookie array path)
- server-initiated RST_STREAM rejects via the `error` listener
- pre-aborted signal rejects synchronously (queueMicrotask path)
- cached session reuse across sequential requests (bumpIdle on hit)
- recovery when the cached session was destroyed between calls
- idle-timer expiry closes the session, next call opens a fresh one
- dispose() is idempotent and a no-op before any request

undici scenarios added:
- POST forwards body bytes and sets `duplex: 'half'`
- bodyless GET omits both `body` and `duplex`
- request.signal forwards through to the dispatcher
- dispatcher object identity preserved (same reference, not cloned)
- POST/PUT/DELETE method propagation
- request.redirect propagation
- ensureFetch caches the import across concurrent requests
- real-undici end-to-end POST (covers dynamic-import branch)
- real-undici non-2xx surfaces as HTTPError
- error path when undici exposes no `fetch` (line 81 throw)

Server-side `stream.on('error', ...)` swallows the expected
RST_STREAM / abort errors during teardown so they don't surface as
unhandled exceptions on the http2 server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

Bench results

✅ No misina row regressed >5% vs base.

benchmark base avg head avg Δ
steady-state GET (200 OK, JSON parse) :: axios 308.45 µs 311.89 µs · +1.1%
steady-state GET (200 OK, JSON parse) :: misina 303.97 µs 306.67 µs · +0.9%
steady-state GET (200 OK, JSON parse) :: ky 291.92 µs 291.61 µs · -0.1%
retry on 503 → 200 :: misina — retry 1× then 200 1.71 ms 1.71 ms · -0.4%
POST JSON body :: native fetch 367.46 µs 365.60 µs · -0.5%
steady-state GET (200 OK, JSON parse) :: ofetch 270.16 µs 268.52 µs · -0.6%
POST JSON body :: ofetch 339.42 µs 336.36 µs · -0.9%
POST JSON body :: ky 463.77 µs 457.06 µs · -1.4%
POST JSON body :: axios 326.03 µs 319.16 µs · -2.1%
createMisina cold start :: createMisina({ use: [bearer, tracing, breaker] }) 1.85 µs 1.80 µs · -3.2%
steady-state GET (200 OK, JSON parse) :: native fetch 341.89 µs 326.14 µs · -4.6%
plugin overhead (no plugins / 1 hook plugin / 3 plugins inc. wrapping) :: misina — bearer + tracing + breaker 321.71 µs 305.23 µs 🟢 -5.1%
plugin overhead (no plugins / 1 hook plugin / 3 plugins inc. wrapping) :: misina — no plugins 291.19 µs 272.64 µs 🟢 -6.4%
hooks overhead (no hooks vs 5 hooks) :: misina — 5 hooks 303.57 µs 283.50 µs 🟢 -6.6%
plugin overhead (no plugins / 1 hook plugin / 3 plugins inc. wrapping) :: misina — bearer 295.73 µs 273.59 µs 🟢 -7.5%
createMisina cold start :: createMisina({ use: [bearer] }) 553.7 ns 510.7 ns 🟢 -7.8%
hooks overhead (no hooks vs 5 hooks) :: misina — no hooks 312.07 µs 281.67 µs 🟢 -9.7%
POST JSON body :: misina 634.11 µs 503.63 µs 🟢 -20.6%
createMisina cold start :: createMisina() 1.20 µs 535.4 ns 🟢 -55.5%

Local Node HTTP server, Apple-class GitHub runner. Numbers fluctuate ±2-3% from runner heat alone — only sustained >5% deltas are signal.

The test referenced `followRedirects: false`, which is not a valid
MisinaOptions key — typecheck failed. Use `redirect: "follow"` to get
the same effect (driver receives the original redirect field instead of
misina intercepting it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@productdevbook
Copy link
Copy Markdown
Owner Author

Driver test coverage looks solid: real http2 server (with proper socket cleanup destroying every accepted socket on teardown — necessary because Http2Server.close lacks closeAllConnections), real http listener for undici. Branch coverage hits the targets (http2 81.13%, undici 80%, driver/ 80.82%). Tests exercise real async paths: RST_STREAM via stream.close(0x02), abort via DOMException AbortError, ensureFetch concurrent-import branch, dispatcher identity propagation, multi-value set-cookie via Headers.append. Disposal is tested for idempotency and pre-request no-op. Caught and fixed one issue: a test referenced followRedirects (not a real MisinaOptions field) — replaced with redirect: 'follow'. Full suite 932/932 green after fix.

@productdevbook productdevbook merged commit 37f0ad0 into main May 3, 2026
5 checks passed
@productdevbook productdevbook deleted the test/driver-coverage branch May 3, 2026 09:32
productdevbook added a commit that referenced this pull request May 3, 2026
* test(driver): cover undici pool / http2 socket / abort branches

Drives the previously-uncovered branches in `src/driver/http2.ts` and
`src/driver/undici.ts` so each file lands at >=80% branch coverage and
the `src/driver` aggregate clears the same bar.

http2.ts: 60.37% -> 81.13% branches.
undici.ts: 70% -> 80% branches.
src/driver overall: 64.38% -> 80.82% branches.

http2 scenarios added:
- host header stripped before sending (server sees only :authority)
- multi-value response headers append (set-cookie array path)
- server-initiated RST_STREAM rejects via the `error` listener
- pre-aborted signal rejects synchronously (queueMicrotask path)
- cached session reuse across sequential requests (bumpIdle on hit)
- recovery when the cached session was destroyed between calls
- idle-timer expiry closes the session, next call opens a fresh one
- dispose() is idempotent and a no-op before any request

undici scenarios added:
- POST forwards body bytes and sets `duplex: 'half'`
- bodyless GET omits both `body` and `duplex`
- request.signal forwards through to the dispatcher
- dispatcher object identity preserved (same reference, not cloned)
- POST/PUT/DELETE method propagation
- request.redirect propagation
- ensureFetch caches the import across concurrent requests
- real-undici end-to-end POST (covers dynamic-import branch)
- real-undici non-2xx surfaces as HTTPError
- error path when undici exposes no `fetch` (line 81 throw)

Server-side `stream.on('error', ...)` swallows the expected
RST_STREAM / abort errors during teardown so they don't surface as
unhandled exceptions on the http2 server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(review): replace nonexistent followRedirects with redirect: follow

The test referenced `followRedirects: false`, which is not a valid
MisinaOptions key — typecheck failed. Use `redirect: "follow"` to get
the same effect (driver receives the original redirect field instead of
misina intercepting it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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