Skip to content

Add tests for core behaviour the suite ran but never checked - #871

Merged
Yaraslaut merged 17 commits into
masterfrom
tests/kill-core-mutation-survivors
Oct 4, 2026
Merged

Yaraslaut merged 17 commits into
masterfrom
tests/kill-core-mutation-survivors

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

A local Mull mutation run over include/morph/core/ on cc00977 scored 73.0%: 790 mutants, 577 killed, 213 survived. A survivor is a change to the library that morph_tests still passes. This PR goes through all 213. Each one has been applied to the header by hand and given a verdict.

Verdict Count
Killed by a new test 92
Killed by removing code that had no effect 3
Already caught by the existing suite (Mull misreported it) 42
Cannot change observable behaviour 74
No deterministic way to test 2

Every new test was checked in both directions. It fails with the mutation applied by hand to the header, and passes against the original code.

No library defect was found. Every survivor that changes behaviour was a test gap, not wrong code.

The 42 misreports. Mull reported these as survivors, but each one fails an existing test once the mutation is applied by hand. The likely cause is that Mull mutates one translation unit's copy of an inline header function and the linker keeps an unmutated copy. That cause is inferred, not proven. Either way, survivors reported in header code need checking by hand before anyone acts on them.

The constant 42 means false for a bool. In LLVM IR, Mull's replacement constant 42 truncates to false for a bool. So flag = false → = 42 leaves the program unchanged, and those mutants are counted as unable to change behaviour.

Changes

  • Library simplifications. Each removes code whose only effect was already guaranteed elsewhere; behaviour is unchanged.
    • bridge.hpp: a currentId == 0 check that dispatchNow already makes with the same error.
    • wire.hpp: peekCallId's sawDigit flag, which only guarded returning a value that is already 0.
    • remote.hpp: nextOpaqueId's initial value, which was always overwritten.
  • TimeoutScheduler documentation. The Handle doc now states that a handle is never 0. DelayAwaiter relies on that.
  • Bridge tests:
    • bind and attach supersession across switch and reconnect
    • inline publication when the backend settles on the owner
    • setExecuteDeadline(0) disarming the deadline and starting no timer thread
    • handler-destruction tracking
    • which dispatcher and registry close the registration latch
  • Backend tests:
    • a synchronous verb on its own strand runs inline
    • destruction requests stop on a running Task handler
    • cancelled waiters finish as failures
    • executeLatencyMs covers the handler's run time
    • trackedPendingCount
    • shared-instance promotion and release
  • RemoteServer and SimulatedRemoteBackend tests:
    • OpaqueIdGenerator is a bijection across both halves
    • drainedWithin(0)
    • an executeTimeout deadline is retired when the reply arrives
    • per-connection release
    • the live-model cap is checked before and around construction
    • the content of the dispatch log lines
    • assignPrimary, private re-bind and cancelPending
  • Tests for the other core headers:
    • rollBackShortWrite and repairTornTail edges
    • peekCallId at zero digits and at the uint64 boundary
    • the payload-shape depth limit for every nesting form, plus FNV-1a reference vectors
    • TimeoutScheduler retiring its loop timers on cancel, close and delay
    • the action gate's hand-off after a held leave
    • the thread pool draining its queue on destruction
    • OwnedState::ask posting to the owner
    • observing when no sink was ever installed

The two survivors with no test are remote.hpp's in-flight slot release, which is only reachable if dispatchExecute throws between reserving the slot and posting, and coroutine.hpp:243, a stop racing the timer arm. Neither has a seam that would make a deterministic test.

Re-running the same Mull campaign on this branch, before the rebase so the base is still cc00977, scores 82.78%: 784 mutants, 649 killed, 135 survived. That is up from 73.0%.

  • The total is 784, not 790, because the three simplifications removed mutated code.
  • Four survivors are new. They sit on lines this branch touched, and none of them can change behaviour.
  • Mull still reports 11 mutants as surviving even though a test here was seen failing against the same mutation applied by hand. They have not been re-checked yet. Either Mull's mutation differs from the one applied by hand, or this is the same header misreporting described above.

🤖 Generated with Claude Code

https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2

Yaraslaut and others added 14 commits October 4, 2026 19:06
…; drop a check dispatchNow already makes

Tests for: the registration latch closing on the process ActionExecuteRegistry
only; held calls rejected with BridgeDestroyedError and attaches dropped (not
issued) on teardown; deregistering one handler keeping the others tracked; a
posted off-owner registration holding calls made meanwhile; an empty
assignHandlerPrimary key promoting nothing; a failed promote logging the
backend's reason; a stale bind reply staying stale after a switch followed by
an attach or a reconnect; inline publish when the backend settles on the owner;
the guiExec owner check; setExecuteDeadline(0) disarming and starting no thread.

executeAttachedVia's continuation no longer re-checks currentId == 0:
dispatchNow rejects an unbound binding with the same error, now pinned by a
test of an attach that settles with no instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…uite ran but never checked

~LocalBackend stopping a running Task handler; a call cancelled while waiting
behind a Task handler finishing as a failure; trackedPendingCount counting
in-flight calls; executeLatencyMs covering the handler's run time; an empty
assignPrimary key filing nothing; a change-aware instance staying notified
after one of two handlers releases it; the adapter running a synchronous verb
inline when re-entered from its own control strand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…backs, and repairTornTail's trim and report

A rollback at offset zero (an empty file) is a real rollback, a second short
write on the same handle must roll back to the shortened file, a file with no
complete record is emptied, and the trim warning carries the discarded byte
count while an intact file logs nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…nt64 edge and the refusal messages

peekCallId's "saw a digit" flag only ever guarded returning a value that is
already 0 when no digit was seen, so it goes. Tests now cover ids containing
the digit 0, the largest id that still fits next to the first one that would
wrap, decode's size refusal naming the refused size, and interpretHelloReply
carrying the peer's own message (or "malformed reply" for an empty one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
… promoted instance dies with its handler

Both ActionDispatcher entry points close the latch when they read the process
dispatcher and leave it open for a locally owned one. An instance promoted by
assignPrimary carries exactly the one attachment of the handler that created
it, so releasing that handler destroys it and frees its key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…el, close and delay retire their loop timers

delay() holds 0 until its timer is armed and cancels whatever it holds on a
stop, so a scheduler handle of 0 would let it cancel another caller's
callback; the Handle doc now says so and a test pins it. Cancelling, destroying
the scheduler, stopping a delay and destroying a suspended delay each retire
the event loop's timer, asserted on the loop's own pending-timer count rather
than only on the callback not running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…into when the post then fails

An executor that runs the posted attach and then throws leaves the await
already settled by the handler the attach installed. The coroutine has to see
the value exactly once, not the post's exception on top of a queued resumption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…drain on destruction, and OwnedState::ask's post

An action started by a leave() that is still running when that leave()
returns starts the next queued action from its own leave(). A ThreadPoolExecutor
destroyed with work queued runs all of it before joining, as its destructor
documents -- the test asserted only that the join returned. OwnedState::ask
called off the owner runs its body on the owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…FNV-1a digest, key decoding by view, and the never-configured observe state

Optional, map key, map value, reflected member and a PayloadShapeTag's inner
shape each cost one level, so nine of any of them render the leaf as the opaque
tag. The fingerprint digest is checked against published FNV-1a vectors, since
a journal's stamp is compared across builds. keyFromString decodes only the
characters its view spans, and ScopedObserveOverride works in a process that
has never installed a sink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…ijection across both halves

nextOpaqueId's `id = 0` was overwritten before its first read; the loop now
declares the candidate where it is drawn.

The existing bijection samples each vary only one half of the counter, so a
permutation that ignores the high word or folds its output to 32 bits still
passes them. A sample of 2^19 counters spread over both words collides ~32
times under such a permutation and never under the real one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
The undecodable-envelope line's size, capped prefix and ellipsis, the
per-request debug line's bodyBytes, and the latency metric's value were
emitted but never read: any of them could report a constant and the suite
stayed green. The logger override and metric sink make each one checkable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…etirement on reply

drainedWithin(0) answers from the in-flight count its request finds; a test
now queues an execute's completion right behind the request, so deferring the
answer to a timer turns it into `true`. An execute that answers before its
executeTimeout must cancel the armed deadline, which otherwise holds the
server alive until it fires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…ap around construction

A connection that has released its one reference to a shared instance cannot
release another connection's; an attach re-pointing onto a key already live
releases the old instance; a register at the cap is refused before any model
is constructed; and a register whose own construction fills the last slot is
refused by the re-test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…and cancelPending

assignPrimary files the instance under its key; binding a private instance
over a live one releases it; cancelPending fails every call still in flight,
not just the most recent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
@Yaraslaut Yaraslaut added no docs update Skip the header<->spec sync gate for this PR area: core Subsystem: core labels Oct 4, 2026
Yaraslaut and others added 3 commits October 4, 2026 19:56
const guards and capture, reserve before the push loop, a private flag
behind a setter, contains() for the log match, named leaks, and a NOLINT
for the by-value parameter ActionCall::localOpAsync's signature requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
…ive the keyed failure

The parked handler stored its completion callback, which holds the call,
which holds the handler: a cycle LeakSanitizer reports once the test ends.
finish() takes the callback out before running it. The bind-path test
recorded the keyed call's failure into a local that went out of scope
before the owner delivered it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2
@Yaraslaut
Yaraslaut merged commit 7993c28 into master Oct 4, 2026
27 checks passed
@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/core/remote.hpp 66.66% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Subsystem: core no docs update Skip the header<->spec sync gate for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant