Re-run the whole-repo mutation campaign and re-ledger it - #139
Conversation
Six behaviours the suite executed but nothing distinguished: - the frozen record is matched on the address a release DERIVES, not the one it records, so a stale pin is the internal group's to name rather than a release reported as undeclared - the internal-consistency check reaches every declared suite, not the first - the derivation clears the derived address's nonce as well as its code - the broadcast carries the suite's recorded pins, so the pre-fork comparison is against a recorded value rather than against itself - the broadcast carries the suite's dependency list - a read that reverts carrying exactly the bytes a successful answer would have carried is refused Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A deployment that answers the same thing on every network passes a matrix that only ever forked the first one, and the single-network mismatch case cannot tell them apart either. The target here agrees on the first network and disagrees on the second, so the failure names the network the loop had to advance to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The one entry on the record described a single library at a commit 211 ahead of HEAD, so it attested to nothing the repo now compiles. Two keys beyond the existing shape, because the run produced two outcomes it had nowhere to put: eleven mutants in `script/Build.sol`'s entry points that no test executes, and two `findDeployBlock` mutants whose search stops narrowing, so the suite never reports at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review. WalkthroughAdded deployment fixtures and regression tests for stale address pins, missing dependencies, snapshot derivation, nonce handling, reverted reads, and multi-network address checks. Appended a mutation-test scan record for the release commit. ChangesDeployment validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The PR adds mutation-test coverage and updates the audit ledger without changing production behavior. The ledger metadata should be aligned with the scan provenance, or the scan should be documented as independent; this is a bounded follow-up rather than a production merge blocker. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…buildscript-base #140 swept named returns out of the repo (#129: this repo does not name return values), and #139 pinned the name list against the declaration. Both landed on the functions this branch renames. Conflicts, and how they were taken: * script/Build.sol - main unnamed generatedContractNames()'s return; this branch renamed that function to snapshotContractNames() as BuildScript's hook. Kept the rename, took main's style: unnamed `returns (string[] memory)` with an explicit `return names;`. generatedContracts() was already main's unnamed form and came through the automatic merge unchanged. * test/concrete/BuildHarness.sol - main's externalGeneratedContractNames() wrapper and this branch's externalSnapshotContractNames() are the same seam onto the renamed function. Kept this branch's, dropped main's: the function it called no longer exists. * test/script/Build.t.sol - merged without markers, but main's testGeneratedContractNamesAreTheDeclarationInOrder called the wrapper that went. Repointed at externalSnapshotContractNames() and renamed to match, so main's positional assertion is not lost. Its doc reference, and the one in testTheCommittedAggregateIsInDeclarationOrder, follow the rename. test/concrete/BuildScriptHarness.sol is not a conflict but its snapshotContractNames() is added by this branch with a named return, so it is unnamed here too, with an explicit return. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`#139` and `#140` landed under this branch, and `#126`'s aggregate writer came with them. Four files conflicted. - `foundry.toml`: both sides add the SAME `./fixture-lib` fs_permissions entry, predicted by this PR's "Not in this PR" note. One entry, with a comment covering all three writers that are pointed there. - `.soldeerignore`: both sides add `/fixture-lib`, in different positions. Deduped, keeping the sorted one. - `src/lib/LibRainDeploySnapshot.sol`: main introduced `LIB_DIR`, `pathForLib` and `releasedLibraryName` for the aggregate writer, so this branch's own `LIB_DIR` is a second declaration of main's and its hand-concatenated paths are main's helper spelled twice. Main's are kept, and the two writers this PR parameterises now build their path with `pathForLib(libDir, libraryName)`. The aggregate writer's NatSpec claimed the other two writers "take no directory and are always `LIB_DIR`", which this PR makes false; it now says they take the same `libDir`. - `test/script/Build.t.sol`: both sides reworded one paragraph. This branch's reason (both entry points rewrite committed files other contracts read) with main's conclusion (nothing below writes anything, which is what main's new reading tests made true). - `test/src/lib/LibRainDeploySnapshot.t.sol`: main added `generatedFilePrefix()` and the aggregate emitter tests where this branch deleted `testWriteReleasedSuitesLibWritesTheLibAtItsPath`. All of main's is kept and the deletion stands — that test wrote the committed `src/lib/LibAddressRegistryReleased.sol`, which is the race this PR removes, and `testTheCommittedReleasedLibIsWhatTheGeneratorEmits` carries its staleness half without writing. The three tests this PR added spell the generated header out literally; they use `generatedFilePrefix()` now. `testTheCommittedAggregateIsWhatTheGeneratorEmits` cited the deleted test by name and now cites its replacement. Return style is main's throughout: unnamed `returns (...)` with an explicit `return`, per #129 and the #140 sweep. Nothing added here names a return, and `forge build --force` emits no solc warning at all. `nix develop -c forge test`: 275 passed / 52 failed, every failure `*_RPC_URL not found`; `grep '[FAIL' | grep -vc '_RPC_URL. not found'` = 0. `git status` clean after the run and `fixture-lib/` empty, so no test writes a committed file. `nix develop -c forge fmt --check` exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #64
The ledger described one library at a commit 211 behind HEAD. This runs the
campaign over everything the repo now compiles and writes the second entry.
Scope and harness
Whole repo at
ed91bfa, 15 units, 264 mutants. Every source file undersrc/andscript/with a body of its own;script/Deploy.solis an emptycontract and has none.
The suite command is
nix develop -c forge testagainst five local anvilchains, one per configured network, each with the chain id the real network
uses and the Zoltu factory at its canonical address, and each with one block
mined before that code is set so the factory first has code at block 2 rather
than at genesis. That runs 307 of the repo's 311 tests. The four it cannot
run need Base archive history back to block 1117029 or Base's genesis WETH9
allocation at block 0.
Fuzz seed pinned (
FOUNDRY_FUZZ_SEED=0x5eed) so a mutant cannot be killed bya seed and survive under another.
The lying harness, and what defused it
Any mutation of a file that feeds a deployed contract's creation code moves
that creation code, so
testSnapshotMatchesSourcefails — on every suchmutant, whatever it is. A filter that includes it reports KILLED for all of
them and measures nothing.
For those four files — both concretes and both interfaces — the suite command
therefore regenerates the rolling snapshots from the MUTATED source first
(
forge script ./script/Build.sol --sig "run()"), and restores the generatedfiles afterwards. The source anchor then has nothing to say, and only a
behavioural test can kill anything.
That it worked is measurable rather than asserted:
A01(the addressregistry's root authority) and
I01(the genesis head's preimage) SURVIVEDunder that command. Neither could survive if the source anchor were what was
doing the killing.
Proof the suite ran
mutation-probeaborts before the first mutant on a red, silent or zero-testbaseline, and scores KILLED only from the suite's own tally or a non-zero exit
with a tally present. Every probe log opens with
baseline: green (307 passed)—(311 passed)for the two run against public RPC endpoints,(312 passed)for the re-probe after the new tests. 241 KILLED verdicts each comefrom a run that reported a failing tally out of 307+ executed tests, and every
one of the 15 units produced at least one KILLED, so no unit is a filter that
matched nothing.
Where a run could not be trusted it is reported as such rather than scored:
two mutants time out because the binary search stops narrowing, and are
NO-RUN.
Results
src/lib/LibRainDeploySnapshot.solsrc/lib/LibRainDeploy.solsrc/concrete/MigrationRegistry.solsrc/abstract/RainDeploySuitesBase.solscript/Build.solsrc/abstract/RainDeployVerifySnapshot.solsrc/concrete/AddressRegistry.solsrc/lib/LibMigrationRegistry.solsrc/abstract/RainDeployBroadcast.solsrc/abstract/RegistryDeploySuites.solsrc/abstract/RainDeployVerifyBase.solsrc/abstract/RainDeployVerifyChain.solsrc/interface/IMigrationRegistryV1.solsrc/lib/LibAddressRegistry.solsrc/interface/IAddressRegistryV1.solThe seven gaps this closes
Each row survived the first pass, has a test added here, and is re-probed
KILLED at
99b9ca9.W12testFrozenSnapshotMatchesTheDerivationNotTheRecordedAddressW13testSnapshotInternallyConsistentchecking only the first declared suitetestSnapshotInternallyConsistentReachesEverySuiteV02testDerivationClearsTheDerivedNonceR06run()deriving the pins it is meant to check, making the pre-fork comparison a value against itselfStalePinDeployleg oftestRunSelectsTheSuiteFromTheEnvBeforeTheKeyNeverDefaultsAndBroadcastsItR07run()dropping the suite's dependency listMissingDependencyDeployleg of the same testL32testCheckResolvedAddressesRevertingAddressAnswerRevertsL41checkResolvedAddressesOnNetworksstopping at the first networktestCheckResolvedAddressesOnNetworksReachesEveryNetworkThe two broadcast legs go inside the existing test rather than beside it, and
answer to the
DEPLOYMENT_SUITEit has already set: that variable isprocess-wide and forge runs tests concurrently, so a second writer of it is a
second racer, which is what this contract's own docstring records having been.
The twenty-three that are not gaps
Equivalent — no behavioural difference (7)
L13deployBlock = highinstead oflow:lowonly rises tomid + 1 <= highandhighonly falls tomid >= low, so they meet exactly.G03,G04a candidate'ssourceCreationCodetaken from its recordedCREATION_CODErather thantype(X).creationCode: identical while thecommitted snapshot is current, which is the tree state every other check
already enforces.
A01the address registry's root authority:CLAUDE.mdstates that settinga real root is an ordinary source change that moves the creation code, the
address, the code hash and the release together. Nothing should refuse it.
I01MIGRATION_HEAD_GENESIS's preimage: the interface constrains it tobeing nonzero, one shared value, and not a migration. All three still hold.
The zero case (
I02) is killed.G01,W14the released-suite concatenation and the record-walk root: thisrepo has frozen no release, so the record is empty, and every root and every
concatenation of two empty lists is the same one.
W14stays that way afterthe first release too — a walk of the wrong root returns empty and passes —
which is the residue of The record root is spelled three times, and a walk of the wrong root makes every record-anchored assertion pass with no subject #81 that survives its fix.
Defensive guards nothing else can distinguish (3)
L19mstore(0, 0)before the factory call: the EVM masks addresses to 160bits everywhere the result is used, so a dirty scratch word changes no
outcome.
L25the!successclause: a failed call leavesdeployedAddresszerobecause the assembly's own
if successguard never writes it, so thezero-address clause catches the same case.
L55NoNetworksindeployAndBroadcast:deployToNetworksraises thesame error for the same input.
Executed by no test (11) —
D05–D15,script/Build.sol'srun()andcutRelease()and the regeneration they call. Nothing in the suite runseither: doing so writes the repo's real generated files, which races the
emitter tests over those paths and would regenerate a stale snapshot out from
under
testSnapshotMatchesSource. This is #132's subject and #138 is open forit; note that #138 makes the BASE's entry points concrete and testable, while
Build.build()'s own wiring —D11–D15— stays unexecuted.Non-terminating (2) —
L11low = midandL12while (low <= high)both stop the binary search narrowing. The suite never reports, which is
NO-RUN rather than a verdict; a hang is not something a forge test can be
written to catch.
Adversarial pass
One candidate, confirmed, already on the record: the four
LibRainDeploySnapshotTestemitter tests race in pairs over two committed
src/lib/files and can zeroone. It was hit here as a baseline flake (2 failures in 12 runs of that
contract on
ed91bfa) before #135 was found to be filed for it, with #137open to fix it by giving the writers a directory.
That race is deliberately not fixed here. #137 fixes it properly, and this
branch touches none of the files #137 touches, so the two do not conflict.
The campaign itself was run with the race removed locally, which is the
conservative direction and not a convenient one: a flake can only ever ADD a
failure, so leaving it in would have scored some mutants KILLED on an
interleaving rather than on an assertion. Every KILLED verdict below therefore
comes from a test that actually discriminated, and the 70/70 on
LibRainDeploySnapshot.sol— the unit those four tests belong to — is a real70, not a flaky one.
No other candidate survived refutation. The reads that looked promising —
tagPrecedesindexingright[i]offleft.length,freezecreating the tagdirectory before its writes, the already-deployed branch skipping the
dependency check — are each documented preconditions or documented decisions
with the guard that makes them safe in the same function.
Ledger
audit/mutation-test-scans.jsongains the second entry. Two keys beyond theexisting shape, because the run produced two outcomes the shape has nowhere to
put:
notExecutedBySuiteandnonTerminating.filedis empty andalreadyFilednames #135, because the one confirmed candidate was already onthe record when the campaign reached it.
QA
testFrozenSnapshotMatchesTheDerivationNotTheRecordedAddress,testSnapshotInternallyConsistentReachesEverySuite,testDerivationClearsTheDerivedNonce,testCheckResolvedAddressesRevertingAddressAnswerReverts,testCheckResolvedAddressesOnNetworksReachesEveryNetwork, and the two new legs oftestRunSelectsTheSuiteFromTheEnvBeforeTheKeyNeverDefaultsAndBroadcastsIt(StalePinDeploy,MissingDependencyDeploy). Each passes unmutated — 311 tests green on this branch, three consecutive runs — and each fails under exactly the mutation it is for: all seven mutants SURVIVED the pass ated91bfabefore these tests existed, and all seven are re-probed KILLED at99b9ca9.line -> mutation -> killing test:RainDeployVerifySnapshot.solrecorded == LibRainDeploy.zoltuAddress(released[j].creationCode)->recorded == released[j].storedDeployedAddress->testFrozenSnapshotMatchesTheDerivationNotTheRecordedAddress;RainDeployVerifySnapshot.soli < suites.length->i < suites.length && i < 1->testSnapshotInternallyConsistentReachesEverySuite;RainDeployVerifyBase.soldeletevm.resetNonce(formulaAddress);->testDerivationClearsTheDerivedNonce;RainDeployBroadcast.solsuite.storedDeployedAddress, suite.storedBytecodeHash->LibRainDeploy.zoltuAddress(suite.creationCode), keccak256(suite.storedRuntimeCode)-> theStalePinDeployleg;RainDeployBroadcast.solsuite.dependencies->new address[](0)-> theMissingDependencyDeployleg;LibRainDeploy.solif (!success || returnData.length != 0x20)->if (returnData.length != 0x20)->testCheckResolvedAddressesRevertingAddressAnswerReverts;LibRainDeploy.solnetwork loopi < networks.length->i < networks.length && i < 1->testCheckResolvedAddressesOnNetworksReachesEveryNetwork.RainDeployVerifySnapshot's docstring states the record is matched "by address: the address a file DECLARES against the address a suite's creation code DERIVES".deriveDeployment's states "the nonce goes too:CREATE2collides on a non-zero nonce as well as on non-empty code".RainDeployBroadcast's states the recorded pins are passed rather than derived because "a guard that compares a value to itself is not a guard", and thatdependenciesis what "refuses to broadcast on any network where one of them has no code".ResolvedAddressReadFailed's states a read that "reverts, answers nothing … or answers something that is not one word cannot be compared, and is never a pass".checkResolvedAddressesOnNetworks's states it "runscheckResolvedAddresseson every network". Every expected value is read off those statements; none is copied from the code under test.audit/mutation-test-scans.jsonin the existing shape, prioritising the units with no ledgered coverage. Covered: whole repo ated91bfa, 15 units and 264 mutants none of which were ledgered before, and the ledger entry. The issue's prioritisation list is stale in the one respect its own verification section flags:script/Build.solnow hastest/script/Build.t.sol, andRainDeployBroadcast.runandLibRainDeploySnapshot.freezeboth have tests.