Skip to content

test: raise mesh-session.ts mutation score to 92.91% - #112

Merged
Mearman merged 8 commits into
mainfrom
fix/mutation-survivors-core
Sep 13, 2026
Merged

test: raise mesh-session.ts mutation score to 92.91%#112
Mearman merged 8 commits into
mainfrom
fix/mutation-survivors-core

Conversation

@Mearman

@Mearman Mearman commented Sep 13, 2026

Copy link
Copy Markdown
Member

Fixes #68

wire-mesh-core#67's baseline scored tokens.ts and revocation-view.ts at 80.00% and 100.00% already (nothing to do there). mesh-session.ts scored 67.32% (145 killed, 26 timeout, 83 survived); this raises it to 92.91% (204 killed, 32 timeout, 18 survived out of 254 scoreable mutants) by tracing every survivor individually and adding a real assertion where the gap was genuine, or confirming and documenting equivalence where it wasn't.

Along the way this caught a real bug in the test suite itself: three "emits a session event" tests awaited a second event, then unconditionally called session.close() afterward. Since close() always emits one final event of its own, a missing emit() in respond()/sendRevocationAnnounce/sendGossipUpdate was silently masked -- the awaited event just resolved later from close()'s trailing emit(), with a frameLog snapshot that still looked correct by coincidence. Fixed with a withinShortWait() helper that races the next event against a short real timer, so the assertion only passes if the event was genuinely available immediately, before close() is ever invoked. Verified by manually re-applying each exact mutation and confirming the fixed tests now fail against it.

Verification: fresh tsc --noEmit, eslint, and pnpm test (13 files, 237 tests, including 75 in mesh-session.test.ts alone) all pass.

The 18 remaining survivors

Each traced individually against the real call graph, not assumed:

  • connection===null guards inside transmit()/ensureRelayPairing(): dead. Every public caller already checks connection===null itself before reaching these; connection is set exactly once and never reset to null.
  • default localHandshake([]) for localHandshakeSent: dead. wireUpConnection unconditionally overwrites this before consume() starts.
  • clearTimeout of handshakeTimer inside applyRemoteHandshake: equivalent -- the timeout callback re-checks handshake.status==="pending" before acting, so an uncleared timer is a no-op once negotiated/rejected.
  • handshake-timeout callback's own status==="pending" check: equivalent for the same reason -- the callback only ever runs at all when clearTimeout hasn't already cancelled it, which happens exactly when the handshake is still genuinely pending; confirmed by manually re-applying the mutation in isolation and tracing why the test still passed correctly.
  • state.status==="connected" check before handleDisconnect on a clean stream end: equivalent -- handleDisconnect's own first line (if (feedCancelled) return) already no-ops the only reachable case where this outer check would matter.
  • state.status==="connected" check in wireUpConnection's consuming.catch handler: very likely equivalent (receive() only rejects while actively connected) but lower confidence than the others -- constructing a counterexample through the public API would need scaffolding disproportionate to the value here.
  • dial===null error message in doConnect: dead -- connect() itself already throws its own error before doConnect is reached when connection!==null; dial is only null for accepted sessions, which always have connection set by construction.
  • left operand of connection===null || state.status!=="connected" (three call sites: transmit's own guard, sendRevocationAnnounce, sendGossipUpdate): logically redundant given the codebase's own invariant that state.status==="connected" is only ever true after connection is set non-null in the same synchronous block -- connection===null always implies the right operand is already true too.
  • pendingManageRequests.delete(requestId) forced to always-true in the timeout race: equivalent under Promise.race semantics -- by the time this differs from real behavior, the outer race has already settled via the real response.
  • peerDeviceIdResolved early-return guard and its boolean flag: equivalent under ECMAScript's own promise-idempotency guarantee -- resolving an already-settled promise a second time is a spec-level no-op.
  • pendingManageRequests.clear()/.delete(requestId) cleanup calls: equivalent for observable behavior -- nextRequestId only increments, so no future request can reuse a stale map entry; re-resolving/re-rejecting an already-settled promise is a no-op.

…se/handshake edge cases

Assert sendGossipUpdate rejects extensions colliding with the addresses
and snapshot-seconds mandatory fields specifically, not just device.
Assert the extension-key pattern rejects trailing garbage after a valid
prefix and a valid suffix reached from a non-domain-qualified start,
proving the regex is anchored at both ends rather than merely searched.

Assert close() while connected finalizes state to closed/"closed by
you" and actually invokes the underlying connection's close(). Add
FakeConnection.isClosed and .endStream() so a clean remote hang-up can
be distinguished from a caller-initiated close in tests.

Assert a frame already queued at the moment close() runs is dropped
rather than applied, and that a second handshake frame arriving after
the first has already settled negotiation cannot re-negotiate. Assert
a rejected handshake's reason is exactly "no shared domains or
version", and that a negotiated handshake stays negotiated once its
own timeout later elapses instead of flipping to unanswered.
Assert close() cancels an armed handshake timeout and a pending
scheduled reconnect (via vi.getTimerCount()), rather than leaving
either running after the session is torn down, and that a reconnect
scheduled but not yet fired never dials again once closed.

Assert close() called while a reconnect is pending still finalizes
state to closed/"closed by you", the same as closing from any other
state. Assert a reconnect attempt whose own dial rejects (as opposed
to the resulting connection later failing) is treated as a genuine
disconnect and reported through state.reason, rather than the
rejection being silently dropped by the scheduler.
…equest backlog delivery

Assert sendManageRequest assigns strictly increasing request-ids
across successive calls, and refuses to send both before the first
connect and again after a connection has failed and closed (not just
before the first connect ever happens).

Assert a request with no timeoutMs given never resolves via the
internal timeout race, by advancing time and then delivering a real
response and checking it still wins.

Assert respond() emits a session event reflecting the sent response
frame, and that a manage-request received before anything was
iterating incomingManageRequests is still delivered once iteration
starts, drawn from its own backlog rather than being lost.

Add a yielded() helper to narrow an IteratorResult to its value
without an unsafe cast, since AsyncIterator's default TReturn=any
otherwise infers `any` for .value even after checking .done.
…aults

Assert the events async iterator's next() result carries done: false
both for a live event delivered after the wait began and for one
already backlogged before anyone started iterating.

Assert acceptMeshSession advertises no addresses when none are given
(rather than falling through to some other default) and labels its
connection state "accepted" when no label option is given. Strengthen
the "refuses connect()" assertion to check the actual rejection
message reachable through the public API, rather than any throw.
…d revocation backlog

Assert both methods emit a session event reflecting the sent frame,
and refuse to run both before the first connect and again after a
connection has failed and closed. Strengthen the revocation-announce
receive test to check done: false on each yielded entry, and add
delivery from the backlog for an entry queued before anyone was
iterating revocationAnnouncements.
Assert that when a dial resolves after the caller has already closed
the session, the resulting connection is closed immediately rather
than wired up (no handshake or self-advert ever sent), matching the
intent that close() cancels any connection attempt still in flight.
…e()-rescued one

The respond()/sendRevocationAnnounce/sendGossipUpdate "emits a session
event" tests were awaiting a second event via nthEvent, then always
calling session.close() afterward. Since frameLog.push() happens
unconditionally before each method's own emit() call, and close()
unconditionally emits one final event of its own, a missing emit() in
any of the three methods was silently masked: the awaited event just
resolved later, from close()'s own trailing emit(), with a frameLog
snapshot that still looked correct by coincidence.

Add a withinShortWait() helper that races the next event against a
short real timer, so the assertion only passes if the event was
already available immediately after the send call, before close() is
ever invoked. Apply it to all three "emits a session event" tests and
reuse it in place of the ad hoc race already inlined in the
close()-drops-a-queued-frame test.

Also add a final-state assertion to the "closes a dial that only
completes after close()" test, covering the state.status ===
"connecting" branch of close()'s own three-way state check -- the
existing assertions only checked the late connection's own side
effects, not that the session's state actually settles to closed.
@Mearman
Mearman marked this pull request as ready for review September 13, 2026 15:32
@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-13T15:36:16.914136Z 663216f Draft marked ready
ℹ️ 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.

resolveDial, a plain let reassigned only inside a Promise executor
nested in an object-property arrow function, lost its non-null
narrowing across several intervening awaits, typing the eventual call
site as never -- caught by tsconfig.node.json's own dedicated
typecheck of test/, which the default tsconfig.json doesn't cover.

Wrapping the resolver in an object property instead of a bare
closed-over variable avoids the narrowing loss entirely, matching the
resolvePeerDeviceId pattern used elsewhere in this same file.
@Mearman
Mearman merged commit 423e920 into main Sep 13, 2026
8 checks passed
@Mearman
Mearman deleted the fix/mutation-survivors-core branch September 13, 2026 15:39
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.

Fix mutations Stryker surfaces in ts/packages/core

1 participant