Skip to content

test(deploy): close the coverage gaps a whole-repo mutation pass found - #19

Open
thedavidmeister wants to merge 1 commit into
mainfrom
2026-07-25-mutation-coverage
Open

test(deploy): close the coverage gaps a whole-repo mutation pass found#19
thedavidmeister wants to merge 1 commit into
mainfrom
2026-07-25-mutation-coverage

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Test-only. No change to src/.

Adversarial mutation-testing pass over the whole repo
(adversarial-mutation-test v0.27.0), scanning 4422e29 — 2 commits past
sol-v0.1.4.

What was done

91 distinct mutations were applied one at a time to
src/lib/LibRainDeploy.sol, each run against the whole suite and restored via
VCS. Every behaviour of all seven functions plus the constants was probed: each
conditional, comparison, boundary, revert path, error argument order, assembly
operand, loop bound and fork restore. Three rounds were run; the third surfaced
no new fillable gap.

  • 67 mutants were killed by the tests that already existed — those tests are
    validated by that, and this PR does not touch them.
  • 18 mutants needed a new or strengthened test — this PR closes all of them.
  • 5 mutants are provably equivalent and 1 is a defensive guard with no
    deterministic discriminator
    (both listed below); no test is added for those.

New tests

Behaviour that no test pinned Test
isStartBlock at block 0 checks only the code hash at that block, with no block before genesis to compare testIsStartBlockGenesisAllocationAtBlockZero
isStartBlock at block 1 still compares against block 0 testIsStartBlockGenesisAllocationAtBlockOne
isStartBlock compares against the immediately preceding block testIsStartBlockOneBlockAfterDeployBlock
findDeployBlock returns the exact deploy block, including when it is the block the fork is at testFindDeployBlockExactZoltuBaseDeployBlock
findDeployBlock leaves the fork on its original block when it reverts DeployedBeforeStartBlock testFindDeployBlockRestoresForkOnDeployedBeforeStartBlockRevert
deployZoltu rejects a successful factory call that left no code testDeployZoltuRevertsEmptyCreationCode
deployZoltu rejects a failed factory call even when its output buffer holds the address of a contract that has code testDeployZoltuRevertsWhenFactoryCallFailsWithAddressData
deployZoltu never reports the zero address as a deployment testDeployZoltuRevertsZeroAddressWithCode
deployZoltu does not forward the caller's value to the factory testDeployZoltuDoesNotForwardValue
deployToNetworks treats a dependency that has code as present testDeployToNetworksPresentDependencyDeploys
deployAndBroadcast broadcasts as the address derived from the given private key, and forwards it to deployToNetworks testDeployAndBroadcastUsesDeployerFromPrivateKey

The new isStartBlock block-0 and block-1 cases use the OP Stack WETH9
predeploy from the Base genesis allocation, which is the only way to reach a
target that already has its code at block 0. testFindDeployBlockExactZoltu\ BaseDeployBlock asserts the exact block (1117029, resolved independently by
binary searching eth_getCode over the Base RPC) rather than round-tripping
through isStartBlock, so the two functions no longer only validate each
other.

Strengthened in place (no duplicates added)

  • testZoltuFactoryCodehash now iterates supportedNetworks() instead of
    forking Arbitrum only. Before this, mutating BASE_SEPOLIA, FLARE or
    POLYGON to a bogus alias survived the entire suite — three of the five
    supported networks were never forked by any test, so nothing pinned that
    their names are real fork aliases or that the factory is actually deployed
    there.
  • testDeployToNetworksMultipleNetworks now asserts the chain left selected
    and the deployed code hash. Its name claims it covers "deploy to every
    network", but it only asserted the returned address — which is identical for
    both networks, so truncating the loop to i < 1, or forking networks[0]
    every iteration, both survived it.

New fixture

MockAddressRevertingFactory — a factory stand-in whose calls always fail,
reverting with exactly the twenty bytes of its own address. It is what makes
the !success term of deployZoltu's guard independently observable.

Survivors deliberately left untested

Provably equivalent:

  • low = startBlockstartBlock + 1: the pre-check proves the target does
    not have the expected code hash at startBlock, so the answer is always
    greater than startBlock.
  • deployBlock = lowhigh: low == high at loop exit.
  • call(..., 12, 20)call(..., 12, 32): mload(0) reads bytes [0, 32)
    either way, so the value is identical for any return-data length.
  • deployedAddress.codehashexpectedAddress.codehash in the final check:
    the two are provably equal at that point on both branches.
  • deployAndBroadcast's own NoNetworks guard: deployToNetworks reverts with
    the identical error, so the guard is redundant.

Not equivalent, but not deterministically discriminable from a test:

  • deleting mstore(0, 0) in deployZoltu. It guards against stale scratch
    memory bleeding into deployedAddress when a factory returns fewer than 20
    bytes. Scratch happens to be zero at that point under the current compiler,
    so its removal is currently unobservable, and a test cannot dirty
    compiler-owned scratch space deterministically. (Actively dirtying it is
    caught — mstore(0, not(0)) is killed by two existing tests.) The guard is
    correct and should stay.

Findings filed separately

The adversarial half of the pass produced three issues, each with a verified
repro: #16, #17, #18. None of them are enshrined by the tests here — in
particular testDeployZoltuRevertsWhenFactoryCallFailsWithAddressData asserts
only the DeployFailed selector, not the reported address that #17 is about,
so it stays green under that fix.

Scan record

audit/mutation-test-scans.json records the scan for org-wide health tracking.

Probe every behaviour of LibRainDeploy by breaking one line at a time and
running the whole suite. Add tests only for the mutants that survived, and
tighten two existing tests whose names claimed coverage their assertions did
not deliver.

New behaviours pinned:
- isStartBlock at block 0 and block 1, using a Base genesis allocation target
- isStartBlock compares against the immediately preceding block
- findDeployBlock returns the exact deploy block, including when it is the
  block the fork is at, and restores the fork when it reverts
- deployZoltu rejects a codeless result, a failed call whose output buffer
  holds a live address, the zero address, and never forwards caller value
- deployToNetworks accepts a dependency that has code
- deployAndBroadcast broadcasts as the address derived from the private key

testZoltuFactoryCodehash now covers every supported network rather than
Arbitrum alone, so base_sepolia, flare and polygon are exercised at all.
testDeployToNetworksMultipleNetworks now asserts the chain left selected and
the deployed code hash, so a truncated loop cannot pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2b628332-a711-482c-9c73-b608ac8431ad

📥 Commits

Reviewing files that changed from the base of the PR and between 4422e29 and 4fb3d13.

📒 Files selected for processing (3)
  • audit/mutation-test-scans.json
  • test/src/lib/LibRainDeploy.t.sol
  • test/src/lib/MockAddressRevertingFactory.sol
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-07-25-mutation-coverage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister thedavidmeister added the ai:reject AI vetter: needs rework (code issue) label Jul 28, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
Reviewed 4fb3d13: reject — Missing QA evidence block (QA-GUIDE §8) — no ## QA section in body or trusted producer comment. The test code itself audits sound: all 11 named tests present and verified discriminating against src/lib/LibRainDeploy.sol, equivalence claims for the 5 untested mutants hold. Add the §8 block (the body's ledger is the content) and re-push. Refs #16 #17 #18.
cost 455 — fork-test suite, full library re-read

@thedavidmeister thedavidmeister added ai:blocked-on AI producer: blocked on a dependency PR and removed ai:reject AI vetter: needs rework (code issue) labels Jul 28, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer
Blocked-on: Blocked on rainlanguage/rainix#289 (fork-RPC failover preflight), which is now ai:design pending a human ruling. Every failing test in this run is a fork-instantiation failure upstream of this diff: vm.createSelectFork could not instantiate a forked environment because drpc returned error code -32001, You've reached the usage limit for your current plan, preceded by HTTP 408 Request timeout on the free plan and HTTP 429 free plan rate limit. Foundry maps one rpc_endpoints alias to exactly one URL and --fork-retries only re-hits that same dead URL, so no code change in this repo can route around an exhausted vendor quota. Goes green once the failover lands on rainix main.

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

Labels

ai:blocked-on AI producer: blocked on a dependency PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant