Skip to content

test: reach 100 percent statement, branch, function, and line coverage - #16

Merged
Mearman merged 10 commits into
mainfrom
test/full-coverage
Sep 13, 2026
Merged

Mearman merged 10 commits into
mainfrom
test/full-coverage

Conversation

@Mearman

@Mearman Mearman commented Sep 13, 2026

Copy link
Copy Markdown
Member

Summary

  • Drives coverage from ~77/62 to 100 percent across every dimension (statements, branches, functions, lines): CcPeer class error paths and resolveTarget branches, the REST facade's SSE stream/error routes/idle-subscriptions/send-by-pid-and-address, UDS transport listen/close ordering, ps-proc-info's alive/lstart branches, paths' XDG/pid-parsing edges, key/registry store corrupt-file handling, file-transfer oversize/missing/directory/cap/sweep paths, envelope's bare-from parse case, and the schema .is() guards' false side.
  • Fixes a real deadlock found while covering UdsTransport.listen().close(): connections were destroyed after server.close()'s callback instead of before, and that callback only fires once every connection has ended — a listener with any open connection would hang on close forever.
  • Closes the last two branch gaps in src/api/server.ts by extracting listeningPort and hostnameOf as pure, exported functions: listeningPort throws instead of silently defaulting to port 0 when the server ever reports something other than an AddressInfo (impossible through our own call site, so the throw path is unit-tested directly rather than left as an uncovered defensive branch); hostnameOf replaces a split(":")[0] fallback chain (whose second ?? was structurally dead code) with indexOf/slice, making every branch a real, constructible input.
  • Pins a protocol-documentation finding (queue-full cap) to its served-config evidence.

Test plan

  • pnpm test:coverage — 100% statements/branches/functions/lines (645/645, 302/302, 154/154, 600/600)
  • eslint, typecheck, build all green
  • CI verify passes
  • Mutation gate (pnpm test:mutation) — running separately, will report before merge

… and the peer class

Adds unit and integration tests for every path the coverage gate flagged: the attached type
guards' rejection sides, hop-chain and dedup edge shapes, errors.ts's taxonomy, ps-proc-info's
alive/lstart branches (own pid, dead pid, EPERM, missing ps binary, in-flight dedup), paths.ts
(XDG_RUNTIME_DIR, non-numeric socket basenames), fs-key-store and fs-registry-store's
corrupt/missing/schema-invalid file handling, uds-transport's connection lifecycle (readLines
end-of-stream, InboundConnection.close, connectWrite against a dead socket), file-transfer's
oversize/refusal/sweep paths, and the CcPeer class's send/subscribeIdle error paths, inbound
frame handling (foreign auth tokens, malformed JSON, unknown control actions, hop-chain
propagation), and the pacer's refill wait.

Fixes a real deadlock found by the new close-lifecycle test: UdsTransport's
ListeningSocket.close() destroyed accepted sockets after awaiting server.close(), but
net.Server#close only invokes its callback once every connection has ended, so closing with an
open connection hung forever. Destroying accepted sockets before calling close fixes it.

Removes two runtime guards that had become structurally unreachable: buildRegistryEntry read
procStart through an optional chain even though start() already guarantees a non-empty value
before calling it, and handleConnection checked this.ownKey for definedness even though it is
only ever invoked as the listener callback registered after ownKey is set. Both now take the
guaranteed value as a parameter instead, turning a runtime fallback that could never fire into
a type-level guarantee.
…vidence

The tengu_harbor_kite_limits dynamic override has never been served to
this account — it is absent from the Statsig evaluations cache — so the
effective peer-guard limits are the code defaults and maxQueuedPeerMessages
is 50. The 55-messages-queued non-firing therefore reflects queue
accounting (messages parked behind the approval dialog do not count
toward the undelivered-peer-message queue) rather than a raised cap.
Extract listeningPort and hostnameOf as pure, exported functions so
their impossible-through-the-public-API sides are directly unit
coverable rather than left as untested defensive branches. listeningPort
throws when the underlying server ever reports a named-pipe address or
null, which our own call site (always TCP host:port) cannot trigger,
surfacing that violated assumption loudly instead of silently returning
port 0. hostnameOf replaces a split()[0] fallback chain (whose second
nullish coalescing was structurally dead, since split always returns
a non-empty array) with indexOf/slice, so every branch is a real,
constructible input rather than a type-only case. Added coverage for
both, plus a POST /messages case exercising the priority and fromMode
optional fields together, reaching 100 percent branches across the
suite. Also drops a dead eslint-disable comment: the shared config's
noInlineConfig makes inline disables inert, so the existing file-scoped
rule override in eslint.config.ts was already the only thing suppressing
the object-assign rule here.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-13T04:25:23.109475Z 7254eb3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…to stryker

Two independent bugs made the mutation gate meaningless: stryker's CLI
only auto-discovers stryker.conf.json/.js/.mjs/.cjs, never .ts, so
"stryker run" with no argument silently ran with command-line defaults
(no thresholds, the whole src tree as the mutate scope) rather than
this repository's own config; and @stryker-mutator/vitest-runner@10.0.0
joins nested test names in a format vitest 5 no longer produces, so a
mutant run's testNamePattern regex matches nothing and every mutant
executes zero tests, misreporting as Survived regardless of whether
the real suite would actually kill it (confirmed directly: manually
reproducing one mutant and running vitest against it directly failed
the covering test exactly as expected, while stryker still reported it
Survived with "Ran 0.00 tests per mutant").

Passing stryker.config.ts explicitly to the CLI fixes the first bug.
For the second, no fixed vitest-runner release exists yet against
vitest 5 as of this pin; downgrading vitest and its coverage provider
to the 4.x line stryker-mutator/vitest-runner was actually built
against removes the test-name mismatch, confirmed by the same
mutant re-reporting Killed once real tests execute again. Both pins
carry a documented reason per the dependency-pinning policy and should
be revisited once an upstream fix ships.
…ma guards

Now that the vitest-runner/vitest version pin gives a trustworthy
mutation score, work through the genuine survivors it surfaces:

- pacer.ts: fold the refill guard's separate zero-elapsed branch into
  Math.max(0, elapsed) and the msUntilNextToken guard into
  Math.max(0, 1 - tokens), removing two comparisons that were only ever
  equivalent-in-effect at their own boundary (both branches produced
  byte-identical state there) with no test able to distinguish them;
  the simplified arithmetic keeps the same behaviour for every real
  input while shedding the unkillable comparison entirely. Added
  coverage for refill running via msUntilNextToken directly (not only
  after a preceding tryReserve), the exact fractional wait value, and a
  clock moving backward.
- hop-chain.ts: multi-id joinChain output (the separator was untested
  with a single-element chain) and appendHop landing exactly at the
  grammar maximum without trimming.
- envelope.ts: the exact escaped-body content (not just absence of the
  raw tag), a multi-entry hop-chain's comma separator in the built
  wire form, and a crafted body carrying an unescaped closing tag
  earlier in its own content — the greedy body capture runs to the
  LAST closing tag, so re-escaping on rebuild changes the string and
  assertRoundTrips must reject it, exactly the case the check exists
  to catch. Also asserts absent optional fields are omitted keys, not
  keys explicitly set to undefined, on both the from-only and
  from-less parses.
- schemas/guards.test.ts: the key-file token test now supplies every
  required field (a missing pidDomain masked the token regex outcome
  entirely) with both a rejecting and an accepting token, and the
  envelope attributes test adds a fromSession charset boundary.
- stryker.config.ts: ignoreStatic, with a documented, independently
  reproduced link to the still-open vitest-runner bug it works around
  (a module-load-time mutant that crashes test collection is
  misreported Survived rather than Killed).
…direct testing

filterRoster awaited each entry's checkEntry call sequentially in a
for-loop; roster() may probe dozens of live sessions per call and each
check is an independent I/O round-trip with no shared state, so
Promise.all runs them concurrently instead, a real latency win as the
roster grows.

checkEntry's own reason field was untestable from outside: filterRoster
only returns the admitted entries, so a corrupted verdict object or a
blanked-out reason string could never surface through the public
return value even though it changed real behaviour internally.
Exporting checkEntry lets a direct test assert the exact verdict shape
and reason for every rejection path.

Also drops two guards that were provably redundant given the schema's
own guarantees: entry.messagingSocketPath is always a non-empty string
by the point the own-socket check runs (the no-socket branch above it
already returned otherwise), so comparing it against an absent
ownSocketPath needs no separate undefined check; entry.procStart is
always a defined, non-empty string, so an absent lstart already fails
a plain inequality against it without a redundant explicit check.
Both guards produced byte-identical results at every real input either
way, adding a branch with no distinguishable behaviour to test against.
None of the three had a production caller: parseEnvelope splits a
captured hop-chain directly (the wire grammar's own regex already
constrains each id to valid hex before the split ever runs, so
parseChain's per-id re-validation was always redundant there), and
serializeAttributes joins the array directly since its own emptiness
check already precedes the call (joinChain's empty-to-undefined branch
never had anywhere to matter). Neither cc-peer.ts nor the package's
published exports referenced any of the three - only their own unit
tests did, which this removes with them. Only appendHop and checkChain
were ever actually exercised by the protocol implementation, and both
already had real coverage against the same live-verified boundaries.

appendHop's own trim also folds into a single Math.max/slice
expression: slicing from a non-positive start is a no-op in JS, so a
chain already at or under the maximum returns unchanged without a
separate length comparison whose true and false branches produced an
identical result at the exact boundary anyway (trimming to N elements
when you already have exactly N is inherently a no-op, regardless of
which branch decided to do it).
Path helpers: exact spoolDir/uploadsDir segments, not just a substring
check. stageFile: the boundary sits at exactly MAX_FILE_BYTES (accepts)
versus one byte over (rejects), the staged filename's sha256 and uuid
segments are each exactly 8 hex characters (an unsliced sha256 or uuid
would break that exact shape), and the written file is owner-only
(0600). materialiseAttachment: an attachment whose file_size alone is
wrong (hash still matches the real bytes) fails integrity verification
on its own, isolating that half of the OR from the sha256 check the
existing test already covered; the delivered path uses the same
8-character prefix shape as staging. capAttachments: a batch of
exactly the cap size passes through whole, the mirror case to the
existing over-the-cap test. sweepSpool: an hour-old file survives
against the real one-day cutoff (the multiply-vs-divide arithmetic
mistake this guards would place the cutoff a fraction of a millisecond
from "now", sweeping it up too); a file landing exactly on the cutoff
survives, since only strictly-older files are removed; and a pass
processing more entries than the sweep batch always leaves at least
one untouched.
Add a dedicated wire.test.ts exercising every schema in wire.ts: one
fully-shaped valid object per schema (kills every literal/enum-member
mutation the fixture's own values happen to use, plus whole-object
and nested-object ObjectLiteral mutations that would otherwise still
validate an empty-required-field input), one deliberately incomplete
object per schema, an out-of-charset from address for every schema
carrying the address regex, and exhaustive membership checks for every
enum (status, state, from_mode, drop reason).

Length-boundary mutations (.min/.max swapped) fall out of using
realistic, moderate-length fixture values throughout rather than
single characters or the exact limit constants: a normal id or slug
string fails both an inverted min() and an inverted max() in the
direction that actually distinguishes them, so no separate boundary
test was needed for most fields.

Extract YieldReasonSchema as its own bare, uncaught enum: the object
field wraps it in .catch("claim"), which recovers a corrupted "claim"
member to exactly "claim" anyway, so testing the field's own parsed
value can never tell a healthy enum from a broken one for that specific
member. The bare schema's safeParse has no such fallback to mask a
failure.

guards.test.ts gains exhaustive .is() coverage for every registry enum
(NameSource, PeerStatus, SessionKind, PeerFeature) alongside a
rejected-value check for each.

stryker.config.ts: replace the earlier ignoreStatic fix with a
narrower exclusion of only define-schema.ts. ignoreStatic solved the
one collection-crashing mutant it was added for, but every schema
definition in this codebase is itself a top-level const, so it
silently zeroed out mutation testing for the entire schemas
directory — confirmed directly: wire.ts and registry.ts reported zero
mutants under ignoreStatic despite having genuine, real survivors
moments earlier. Excluding just the three-line helper keeps that
side effect from reaching the schemas it don't apply to.
CI's mutation gate failed on a clean checkout with "Cannot find
TestRunner plugin vitest. In fact, no TestRunner plugins were loaded" -
stryker's plugin auto-discovery didn't resolve
@stryker-mutator/vitest-runner against a fresh pnpm install's strict,
non-flat node_modules layout, even though the identical config ran
fine locally against an already-populated node_modules. Naming the
plugin explicitly in the plugins array removes the dependency on
auto-discovery finding it either way.
@Mearman
Mearman merged commit 2641ed3 into main Sep 13, 2026
4 checks passed
@Mearman
Mearman deleted the test/full-coverage branch September 13, 2026 05:24
@exaclaude

exaclaude Bot commented Sep 13, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.1.6 🎉

The release is available on:

Your semantic-release bot 📦🚀

@exaclaude exaclaude Bot added the released label Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant