Skip to content

test(stream): branch coverage >=80% on src/stream#118

Merged
productdevbook merged 1 commit into
mainfrom
test/stream-coverage
May 3, 2026
Merged

test(stream): branch coverage >=80% on src/stream#118
productdevbook merged 1 commit into
mainfrom
test/stream-coverage

Conversation

@productdevbook
Copy link
Copy Markdown
Owner

Summary

Test-only PR. Lifts branch coverage on src/stream/ from 77.14% to 91.42% with 23 new focused tests targeting branches identified by the v8 coverage HTML report.

Coverage delta (src/stream/index.ts)

Metric Before After
Statements 88.55% (178/201) 97.01% (195/201)
Branches 77.14% (108/140) 91.42% (128/140)
Functions 75% (15/20) 90% (18/20)
Lines 92.21% (154/167) 98.20% (164/167)

src/stream/ is a single file (index.ts), so per-file numbers equal the aggregate.

Test scenarios added (test/stream-coverage.test.ts, +425 LOC)

sseStream — additional spec branches

  • yields an event that has only an id and no data lines
  • yields an event that has only a retry and no data lines
  • treats a line with no colon as field-name-with-empty-value (HTML §9.2.6 step 6)
  • treats data (no colon) as data:"" and joins with subsequent data: x

linesOf — buffer / null-body branches

  • yields a trailing line ending in \r when the stream ends without \n
  • returns immediately when the response has a null body (linesOf / sseStream / ndjsonStream)

ensureDisposable surface

  • wrapped iterator exposes Symbol.asyncIterator + Symbol.asyncDispose

sseStreamReconnecting — reconnect branches

  • falls back to default 3000ms reconnectDelayMs when omitted
  • composes init.signal with the external abort signal
  • composeAbort early-returns the already-aborted a signal
  • composeAbort early-returns the already-aborted b signal
  • composeAbort propagates a mid-flight a.abort() to the composed signal
  • clears the pending sleep timer when the external signal aborts during backoff
  • stops after maxRetries failures (failures > maxRetries branch)

accumulateAnthropicMessage — additional branches

  • malformed JSON payload silently skipped (try/catch arm)
  • text_delta on block without prior text initializes from empty string (text ?? "" branch)
  • content_block_start without content_block field tolerated
  • message_start without message field tolerated
  • content_block_delta on missing block tolerated (!block guard)
  • message_delta with neither delta nor usage tolerated
  • usage merges across multiple message_delta events

Remaining uncovered lines

  • Lines 37-45 in ensureDisposable: the wrapped fallback that activates only on runtimes lacking native Symbol.asyncDispose on AsyncGenerator.prototype (i.e. pre-Node 24). The public API (sseStream, ndjsonStream, linesOf) returns native AsyncGenerators on Node 22+ which already expose Symbol.asyncDispose, so this branch is unreachable from outside without monkey-patching the prototype. Acceptable known gap; the wrapped object's surface is exercised through public iteration.

Findings (not fixed — test-only PR)

  • None. No real bugs surfaced; all targeted branches behave per spec/intent.

Test plan

  • pnpm vitest run test/stream-coverage.test.ts test/sse-reconnect.test.ts test/stream-spec.test.ts test/stream-edges.test.ts test/stream-accumulators.test.ts test/stream-abort-response.test.ts test/stream-retry.test.ts — 66 passed
  • pnpm exec vitest run (full suite) — 895 passed (note: pre-existing flake in test/abort-reason.test.ts > "our own timeout fires TimeoutError, not AbortError" reproduces ~1/10 runs on this machine; unrelated to this PR)
  • pnpm typecheck — clean
  • pnpm exec oxlint test/stream-coverage.test.ts — 0 warnings, 0 errors

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

Bench results

⚠️ 2 misina row(s) >5% slower than base.

benchmark base avg head avg Δ
steady-state GET (200 OK, JSON parse) :: misina 221.43 µs 250.13 µs 🔴 +13.0%
createMisina cold start :: createMisina() 453.6 ns 492.3 ns 🔴 +8.5%
POST JSON body :: ofetch 244.34 µs 259.03 µs 🔴 +6.0%
POST JSON body :: ky 333.13 µs 352.13 µs 🔴 +5.7%
POST JSON body :: native fetch 250.29 µs 261.08 µs · +4.3%
retry on 503 → 200 :: misina — retry 1× then 200 1.53 ms 1.59 ms · +3.8%
steady-state GET (200 OK, JSON parse) :: axios 225.86 µs 232.32 µs · +2.9%
POST JSON body :: misina 374.64 µs 382.91 µs · +2.2%
hooks overhead (no hooks vs 5 hooks) :: misina — no hooks 212.75 µs 215.19 µs · +1.1%
POST JSON body :: axios 236.79 µs 238.51 µs · +0.7%
hooks overhead (no hooks vs 5 hooks) :: misina — 5 hooks 211.91 µs 212.52 µs · +0.3%
plugin overhead (no plugins / 1 hook plugin / 3 plugins inc. wrapping) :: misina — no plugins 206.28 µs 205.98 µs · -0.1%
steady-state GET (200 OK, JSON parse) :: ofetch 199.03 µs 198.48 µs · -0.3%
plugin overhead (no plugins / 1 hook plugin / 3 plugins inc. wrapping) :: misina — bearer + tracing + breaker 235.42 µs 233.80 µs · -0.7%
createMisina cold start :: createMisina({ use: [bearer] }) 466.7 ns 462.4 ns · -0.9%
steady-state GET (200 OK, JSON parse) :: ky 215.62 µs 213.40 µs · -1.0%
steady-state GET (200 OK, JSON parse) :: native fetch 240.37 µs 230.53 µs · -4.1%
plugin overhead (no plugins / 1 hook plugin / 3 plugins inc. wrapping) :: misina — bearer 223.18 µs 210.91 µs 🟢 -5.5%
createMisina cold start :: createMisina({ use: [bearer, tracing, breaker] }) 1.75 µs 1.59 µs 🟢 -9.1%

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

…anches

Lift branch coverage on src/stream from 77.14% → 91.42% with 23 new
focused tests targeting:

- SSE: id-only / retry-only events (no data lines), no-colon field lines,
  null-body responses on sseStream/ndjsonStream/linesOf, trailing CR
  flush on linesOf.
- sseStreamReconnecting: default 3000ms reconnectDelayMs fallback,
  composeAbort branches (pre-aborted init.signal, pre-aborted external
  signal, mid-flight a.abort), sleep timer cancellation on external
  abort during backoff, maxRetries exhaustion path.
- accumulateAnthropicMessage: malformed JSON skip, text_delta on
  freshly-created block (text ?? '' nullish), missing content_block
  field, missing message field on message_start, orphaned
  content_block_delta, message_delta with neither delta nor usage,
  usage merging across multiple message_delta events.
- ensureDisposable surface: confirms wrapped iterator exposes
  Symbol.asyncIterator + Symbol.asyncDispose.

Remaining uncovered lines (37-45) are the ensureDisposable fallback
that activates only on runtimes lacking native Symbol.asyncDispose on
AsyncGenerator (pre-Node 24); unreachable from public API on Node 22+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@productdevbook productdevbook force-pushed the test/stream-coverage branch from 6a9f65d to ffd0a85 Compare May 3, 2026 09:02
@productdevbook
Copy link
Copy Markdown
Owner Author

Test-only PR adds 23 focused tests for src/stream/index.ts.

Verified locally:

  • Full suite: 895/895 pass on Node 24.
  • Branch coverage on src/stream/index.ts with full suite: 91.42% (was 77.14%, +14.28pp), exceeds the >=80% bar.
  • Lint: clean (5 pre-existing warnings unchanged). Typecheck: clean.

Test quality is strong. Branches targeted are real and well-described; assertions are specific (token counts, call counts, elapsed-time bounds, merged-shape).

Decision on the lone remaining gap (lines 37-45, the ensureDisposable wrapped-object fallback): LEFT AS-IS. The writer's note that this is dead on Node 22+ is incorrect — V8 added Symbol.asyncDispose on AsyncGenerator.prototype in Node 24, not 22. AGENTS.md baseline is Node 22.11 (matrix runs both 22 and 24 in CI), so the fallback is genuinely live on the LTS leg. Issue #86 / commit 8fb5030 documents that removing it broke CI on Node 22 previously. Coverage isn't gated by CI, so leaving the gap is honest and safer than deleting working code or annotating with c8 ignore in a test-only PR.

Approving via merge.

@productdevbook productdevbook merged commit 2ec7c78 into main May 3, 2026
5 checks passed
@productdevbook productdevbook deleted the test/stream-coverage branch May 3, 2026 09:06
productdevbook added a commit that referenced this pull request May 3, 2026
…anches (#118)

Lift branch coverage on src/stream from 77.14% → 91.42% with 23 new
focused tests targeting:

- SSE: id-only / retry-only events (no data lines), no-colon field lines,
  null-body responses on sseStream/ndjsonStream/linesOf, trailing CR
  flush on linesOf.
- sseStreamReconnecting: default 3000ms reconnectDelayMs fallback,
  composeAbort branches (pre-aborted init.signal, pre-aborted external
  signal, mid-flight a.abort), sleep timer cancellation on external
  abort during backoff, maxRetries exhaustion path.
- accumulateAnthropicMessage: malformed JSON skip, text_delta on
  freshly-created block (text ?? '' nullish), missing content_block
  field, missing message field on message_start, orphaned
  content_block_delta, message_delta with neither delta nor usage,
  usage merging across multiple message_delta events.
- ensureDisposable surface: confirms wrapped iterator exposes
  Symbol.asyncIterator + Symbol.asyncDispose.

Remaining uncovered lines (37-45) are the ensureDisposable fallback
that activates only on runtimes lacking native Symbol.asyncDispose on
AsyncGenerator (pre-Node 24); unreachable from public API on Node 22+.

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