Skip to content

AMT coverage: g7-libdescribedbymeta-emitf - #152

Open
thedavidmeister wants to merge 3 commits into
mainfrom
2026-08-22-amt-g7-libdescribedbymeta-emitf
Open

AMT coverage: g7-libdescribedbymeta-emitf#152
thedavidmeister wants to merge 3 commits into
mainfrom
2026-08-22-amt-g7-libdescribedbymeta-emitf

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Adversarial mutation-test coverage for group g7: LibDescribedByMeta.emitForDescribedAddress, LibCopyArtifacts, the CopyArtifacts script, the interface/constant surface (IMetaV1_2, IMetaBoardV1_2, IDescribedByMetaV1, META_MAGIC_NUMBER_V1) and the subgraph.yaml manifest data under its Solidity oracle. Base commit bba50a7.

Probe: mutation-probe (adversarial-mutation-test flake), suite = the rainix-sol lane's build + artifact regeneration + forge test, with regeneration running INSIDE the per-mutant suite so the committed-ABI freshness oracle is never comparing against a stale copy, and the committed-abi dir reset per mutant so one mutant's regenerated residue cannot leak into the next verdict.

Behaviour matrix

Behaviour Mutation Verdict Killing test
A: hash = keccak256(add(meta,0x20), mload(meta)) offset dropped (hash covers length word) KILLED pre-existing testEmitForDescribedAddressHappy, testEmitForDescribedAddressMismatch
A: hash = keccak256(add(meta,0x20), mload(meta)) length pinned to 0x20 KILLED pre-existing testEmitForDescribedAddressHappy, testEmitForDescribedAddressMismatch
A: actual != expected => revert MetadataMismatch(described, expected, actual) condition inverted KILLED pre-existing testEmitForDescribedAddressHappy, testEmitForDescribedAddressMismatch
A: actual != expected => revert MetadataMismatch(described, expected, actual) check removed KILLED pre-existing testEmitForDescribedAddressMismatch
A: actual != expected => revert MetadataMismatch(described, expected, actual) error args swapped KILLED pre-existing testEmitForDescribedAddressMismatch
A: on match, external call metaboard.emitMeta(subject, meta) call deleted KILLED pre-existing testEmitForDescribedAddressHappy
A: subject = described address, right-aligned subject = caller KILLED pre-existing testEmitForDescribedAddressHappy
A: subject = described address, right-aligned left-aligned bytes20 cast KILLED pre-existing testEmitForDescribedAddressHappy
A: meta forwarded verbatim trailing byte appended KILLED pre-existing testEmitForDescribedAddressHappy
B: contracts() = exactly {IMetaBoardV1_2, IDescribedByMetaV1} IMetaBoardV1_2 renamed to IMetaV1_2 KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes
B: contracts() = exactly {IMetaBoardV1_2, IDescribedByMetaV1} IDescribedByMetaV1 replaced by duplicate SURVIVED -> KILLED new testCommittedAbiDirMatchesContracts
B: livePath = out/.sol/.json out/ prefix wrong KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes
B: livePath = out/.sol/.json .sol/ segment dropped KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes
B: committedPath = crates/bindings/abi/.json escapes the abi dir KILLED pre-existing testArtifactsCommitted (fs_permissions guard)
B: committedPath = crates/bindings/abi/.json suffix drifts to .abi.json SURVIVED -> KILLED new testCommittedAbiDirMatchesContracts
B: extractStable jq argv (3 args) argv shrunk to 2 KILLED pre-existing testArtifactsCommitted
B: extractStable filter keeps only abi + bytecode.object + deployedBytecode.object full bytecode objects kept SURVIVED -> KILLED new testCommittedArtifactIsTheStableSubset
B: extractStable filter keeps only abi + bytecode.object + deployedBytecode.object abi key dropped SURVIVED -> KILLED new testCommittedArtifactIsTheStableSubset
C: run() loop / vm.exists+removeFile / writeFile to committedPath UNPROBED harness, not target: the script IS the regen step every probe's suite runs; mutating it degrades the probe rather than measuring the suite (campaign exclusion)
D: META_MAGIC_NUMBER_V1 literal 0xff0a89c674ee7874 off by one SURVIVED -> KILLED new testMetaMagicNumberV1PinnedToSpec
D: MetaV1_2 event (address,bytes32,bytes), nothing indexed sender made indexed KILLED pre-existing testEmitMetaHappy, testEmitMetaEmptyBody (raw topics/data pin); now also pinned in the artifact by new testMetaV1_2DeclaresNothingIndexed
D: MetaV1_2 event selector = keccak("MetaV1_2(address,bytes32,bytes)") re-typed params UNPROBED as src mutation: every selector-changing re-type fails compilation at the emit site in LibIMetaBoardV1_2 (compilation is the oracle); the manifest side of the selector pin is probed below (E)
D: emitMeta(bytes32,bytes) ABI signature re-name/re-type/mutability UNPROBED: every signature-altering mutation is a compile error via TestMetaBoard's override and call sites; surface additionally pinned by new testIMetaBoardV1_2FunctionSurface
D: describedByMetaV1() view returns (bytes32) view dropped KILLED pre-existing testEmitForDescribedAddressHappy (expectEmit sees the no-longer-static call as "the next call"); now pinned directly by new testIDescribedByMetaV1FunctionSurface
E: exactly one dataSource second dataSource appended KILLED pre-existing manifestJson guard via testManifestIndexesTheInterfaceEvent, testManifestAbiIsAnArtifactThisRepoPublishes, testManifestSourceCarriesNoDeploymentFact
E: exactly one eventHandler second handler appended KILLED pre-existing testManifestIndexesTheInterfaceEvent
E: handler event == MetaV1_2(address,bytes32,bytes), hashing to the interface selector re-typed to uint256 subject KILLED pre-existing testManifestIndexesTheInterfaceEvent
E: source.abi resolves to exactly one mapping.abis entry with file ../out/IMetaBoardV1_2.sol/IMetaBoardV1_2.json source.abi names no entry KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes
E: source.abi resolves to exactly one mapping.abis entry with file ../out/IMetaBoardV1_2.sol/IMetaBoardV1_2.json file points at wrong artifact KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes
E: source.abi resolves to exactly one mapping.abis entry with file ../out/IMetaBoardV1_2.sol/IMetaBoardV1_2.json name declared twice KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes
E: indexed interface is a member of contracts() contracts()[0] renamed (shared mutant with B) KILLED pre-existing testManifestAbiIsAnArtifactThisRepoPublishes (isPublished loop)
E: no address/startBlock key anywhere address added under source KILLED pre-existing testManifestSourceCarriesNoDeploymentFact
E: no address/startBlock key anywhere startBlock added under source KILLED pre-existing testManifestSourceCarriesNoDeploymentFact
E: every network key == template, dataSources[0].network read at its path network: matic KILLED pre-existing testManifestSourceCarriesNoDeploymentFact
E: every network key == template, dataSources[0].network read at its path network key deleted KILLED pre-existing testManifestSourceCarriesNoDeploymentFact
E: every network key == template, dataSources[0].network read at its path network residue in a templates block KILLED pre-existing testManifestSourceCarriesNoDeploymentFact

Baseline repair (pre-existing red)

Adding a new bytes spelling of the magic literal raised forge's fuzz-dictionary hit rate and turned three PRE-EXISTING LibMeta fuzz tests red on unmutated code (seed 0x5f8fac0b...): their negative halves assumed arbitrary fuzzed bytes never carry the magic prefix, while the magic is a PUSH8 in the suite's own bytecode and therefore in the fuzz dictionary. Fixed by enforcing the premise (the vm.assume(!isRainMetaV1(...)) idiom testEmitMetaNotRainMeta already uses) and, for testIsRainMetaV1_2Fuzz, by deterministically breaking a fuzzed prefix so the negative half keeps asserting on every run. Filed as #150. No assertion was matched to buggy behaviour; the library behaviour is correct, the tests' premise was not.

Also filed from the adversarial pass: #151 (manifest mapping.entities omits Transaction, which the handler writes).

QA

  • Discriminating tests: testCommittedAbiDirMatchesContracts, testCommittedArtifactIsTheStableSubset, testMetaMagicNumberV1PinnedToSpec, testMetaV1_2DeclaresNothingIndexed, testIMetaBoardV1_2FunctionSurface, testIDescribedByMetaV1FunctionSurface — each fails on base under its target mutation (probe pass 2 ran --only over the five pass-1 survivors against the strengthened suite: 5/5 KILLED with the new test named as killer by the probe's fail-pattern) and all are green on the unmutated baseline of every pass.
  • Mutations applied: 32 exact-string mutants; the full behaviour -> mutation -> killing-test mapping is the matrix above. Final full probe against the shipped suite: 32/32 KILLED, 0 SURVIVED, 0 NO-RUN, 0 HARNESS-ERROR, baseline green (22 tests). Pass 1 ran the PRE-EXISTING suite only, before any new test existed: 27/32 killed; survivors were contracts()-duplicate, committedPath-suffix-drift, extractStable-filter x2 and magic-literal — exactly the five behaviours the new tests pin. Per-verdict suite ran artifact regeneration inside it (git checkout -- crates/bindings/abi && git clean -fdq crates/bindings/abi && forge build && (forge script script/CopyArtifacts.sol --ffi || true) && forge test under nix develop) so no verdict was read off a stale committed artifact; the mutants file is probe input and deliberately not committed.
  • Oracle: intent, independent of the implementation — the committed-set literals restate what crates/bindings/src/lib.rs consumes (the way SubgraphManifest.t.sol restates the indexed signature); the stable-subset shape is pinned to literal jq key lists and cross-read against the live artifact with the test's own jq spellings, never extractStable's filter; the magic preimage is the metadata-v1 spec literal, independently spelled by crates/cli/src/meta/magic.rs and subgraph/tests/metaBoard.test.ts; the ABI-surface expectations come from the interface NatSpec contracts (all MetaV1_2 inputs unindexed, describedByMetaV1 view) read from the built artifact exactly as alloy::sol! and graph codegen consume it. The LibMeta premise fixes assert those tests' own documented claims ("without the prefix"), not observed behaviour.
  • Category check: campaign slice g7 lists 22 behaviours over 5 units; 19 probed and KILLED (matrix), 3 recorded UNPROBED with reasons (CopyArtifacts script is the probe's own regen harness, per campaign exclusion; event-selector re-types and emitMeta signature changes are compile-enforced by in-tree emit/override sites, and their ABI surface is now pinned by the new AbiSurface tests). Findings filed: LibMeta fuzz tests assert non-meta behaviour over inputs that CAN be rain meta; a seed emitting the magic prefix turns the suite red on unmutated code #150 (LibMeta fuzz premise hole, fixed here), subgraph.yaml mapping.entities omits Transaction, which handleMetaV1_2 writes on every event #151 (manifest entities omission, flagged only).

Summary by CodeRabbit

  • Tests
    • Added validation for public interface signatures, event definitions, mutability, and metadata constants.
    • Strengthened metadata detection tests to handle magic-prefix inputs consistently and avoid false failures.
    • Added checks ensuring committed artifacts match the expected contract set and contain accurate stable ABI and bytecode data.
    • Added safeguards confirming required artifacts exist and preventing untracked artifact files.

baku-ccron and others added 3 commits August 22, 2026 17:56
The non-meta halves of testIsRainMetaV1_2Fuzz,
testCheckMetaUnhashedV1_2Fuzz and testCheckMetaHashedV1_2BadMagicGoodHash
all treated arbitrary fuzzed bytes as never carrying the rain magic
prefix. The magic number is a PUSH8 in the suite bytecode, so it sits in
the fuzz dictionary, and any seed that emits it as the head of a bytes
argument turned the suite red on unmutated code:

    args=[0xff0a89c674ee7874] -> isRainMetaV1 is true, checkMeta* passes,
    the expected NotRainMetaV1 revert never comes.

The two revert tests now enforce the premise with the same
vm.assume(!LibMeta.isRainMetaV1(...)) idiom testEmitMetaNotRainMeta
already uses; the isRainMetaV1 test instead breaks a fuzzed prefix
deterministically (the magic starts 0xff, a zeroed first byte is never
the prefix) so its negative half keeps asserting over every run rather
than discarding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
testArtifactsCommitted proves every artifact contracts() names is fresh,
which a mutation probe shows cannot see:

- a name dropped from (or duplicated in) contracts(): the committed copy
  of the dropped artifact stays behind as an orphan nothing regenerates,
  still compiled into the rust crate (probe: contracts()[1] replaced by
  a duplicate SURVIVED);
- committedPath drifting inside the permitted directory: the regenerated
  copies land at the drifted paths and the freshness test reads its own
  writes back (probe: .json -> .abi.json SURVIVED);
- extractStable filter drift: the committed subset is compared against
  the same filter that wrote it (probe: keeping the full bytecode
  objects SURVIVED, dropping the abi key SURVIVED).

testCommittedAbiDirMatchesContracts pins contracts() to the two
artifacts crates/bindings/src/lib.rs consumes, spelled as literals the
way SubgraphManifest.t.sol spells the indexed signature, and holds the
directory to exactly that set. testCommittedArtifactIsTheStableSubset
pins the committed shape to literals and the kept values to the live
artifact with this file's own jq spellings, so the filter under test
has nothing to agree with but the intent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every Solidity test derives its meta inputs from META_MAGIC_NUMBER_V1,
so no test could see the literal itself drift (probe: off-by-one on the
constant SURVIVED the whole suite). MetaMagicNumberV1.t.sol spells the
eight spec bytes as a preimage, the one place the sol lane can catch a
mutation to either side, aligned with the independent spellings in
crates/cli/src/meta/magic.rs and subgraph/tests/metaBoard.test.ts.

AbiSurface.t.sol pins the artifact surface the consumers bind against:
MetaV1_2 keeps all three inputs unindexed (indexing one re-layouts every
log under alloy::sol! and graph codegen decoders), emitMeta is exactly
(bytes32,bytes) nonpayable, and describedByMetaV1 is exactly () view
returns (bytes32) — view being what lets tooling eth_call a described
contract for its immutable hash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 619d76df-b80a-43cb-aae2-bd08b68ff5a8

📥 Commits

Reviewing files that changed from the base of the PR and between bba50a7 and 1dc3bd9.

📒 Files selected for processing (6)
  • test/interface/AbiSurface.t.sol
  • test/interface/MetaMagicNumberV1.t.sol
  • test/lib/LibMeta.checkMetaHashedV1_2.t.sol
  • test/lib/LibMeta.checkMetaUnhashedV1_2.t.sol
  • test/lib/LibMeta.isRainMetaV1_2.t.sol
  • test/script/CopyArtifacts.t.sol

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The PR adds tests for ABI surfaces, the metadata magic number, fuzz-test premises, and committed artifact integrity. The artifact tests compare committed files with live build outputs and enforce the consumed artifact set.

Changes

Interface and artifact validation

Layer / File(s) Summary
Pin interface contracts
test/interface/AbiSurface.t.sol, test/interface/MetaMagicNumberV1.t.sol
The tests pin event indexing, function signatures, mutability, return types, and the V1 metadata magic number.
Constrain metadata fuzz cases
test/lib/LibMeta.checkMetaHashedV1_2.t.sol, test/lib/LibMeta.checkMetaUnhashedV1_2.t.sol, test/lib/LibMeta.isRainMetaV1_2.t.sol
The fuzz tests exclude valid metadata prefixes or modify them before asserting rejection.
Validate committed artifacts
test/script/CopyArtifacts.t.sol
The tests validate the consumed artifact list, committed files, directory entries, stable artifact keys, and live ABI and bytecode values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 1dc3b

The PR strengthens adversarial test coverage and makes fuzz-test premises deterministic without changing production behavior; the supplied checks are green and all targeted mutations are detected, so no actionable merge-blocking risk remains beyond normal review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the pull request as adversarial mutation-testing coverage for group g7, which matches the stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (6 skipped: 6 unsupported.)
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-22-amt-g7-libdescribedbymeta-emitf

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

Copy link
Copy Markdown
Contributor Author

Independent verification from a second g7 worker in the same campaign: a from-scratch probe of this slice — 27 exact-string mutants written blind to this branch — corroborates the matrix above.

Pass 1, pre-existing suite only, base bba50a7: 21/27 killed. Survivors were exactly the five self-referential oracles this PR pins: contracts() dropping IDescribedByMetaV1, committedPath drifting inside the permitted dir (.alt.json), the extractStable filter (deployedBytecode dropped; identity filter keeping the whole non-deterministic artifact), and the magic literal off by one. One deliberate NO-RUN: emitMeta bytes32uint256 re-type dies as solc E9553 at the LibDescribedByMeta call site — compile-enforced, matching the matrix's UNPROBED reasoning for D.

Pass 3, same 27 mutants against this branch (head 1dc3bd9): 26/27 KILLED, 0 SURVIVED, baseline green (22 tests). Probe-named killers agree with the matrix: testCommittedAbiDirMatchesContracts (contracts() drop AND the committedPath drift), testCommittedArtifactIsTheStableSubset (both filter mutants — re-verified by targeted run), testMetaMagicNumberV1PinnedToSpec (magic off-by-one).

Attribution spot-checks on the pre-existing kills also agree: hash/guard/error-args/subject/verbatim mutants die to testEmitForDescribedAddressHappy/Mismatch; indexed-sender dies to testEmitMetaHappy (raw topic count) and testEmitMetaEmptyBody (raw log data) even before the new artifact pin runs; view-drop dies to testEmitForDescribedAddressHappy via the expectEmit next-call/staticcall distinction; the nine manifest mutants die to the three SubgraphManifest tests as listed.

Divergences: none. Two additional findings filed from this pass's adversarial half: #205 (cross-lane residue that remains after testCommittedAbiDirMatchesContracts, narrowed in a follow-up comment there) and #206 (MetaV1 entity id collision in the subgraph handler, latent under the single-dataSource pin).

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.

1 participant