diff --git a/script/config/VerifyChain.s.sol b/script/config/VerifyChain.s.sol index e206a24..e651f23 100644 --- a/script/config/VerifyChain.s.sol +++ b/script/config/VerifyChain.s.sol @@ -423,7 +423,8 @@ contract VerifyChain is Script { if (isEvm) { _checkApi(name, json); bool rpcOk = _checkRpc(json); - if (rpcOk) _checkOnChainCode(name, json); + // Called unconditionally: the rung self-gates on the fork so a failed RPC still gets a line. + _checkOnChainCode(name, json); _checkRegistryAndExtras(name, json); _checkMesh(name, json, projectJson); _checkLanesOnChain(name, json, projectJson); @@ -869,7 +870,12 @@ contract VerifyChain is Script { // ---------------------------------------------------------------- 5. ON-CHAIN function _checkOnChainCode(string memory name, string memory json) private { - if (!s_forked) return; + if (!s_forked) { + _skipUnverified( + "on-chain: code checks need an RPC (no fork) - router/rmnProxy/tokenAdminRegistry/registryModuleOwnerCustom/link not read" + ); + return; + } string[5] memory keys = ["router", "rmnProxy", "tokenAdminRegistry", "registryModuleOwnerCustom", "link"]; uint256 bad = 0; for (uint256 i = 0; i < keys.length; i++) { @@ -1456,11 +1462,12 @@ contract VerifyChain is Script { return s_skips; } - /// @notice Test hook: drives the three RPC-gated rungs in their no-fork state and returns the + /// @notice Test hook: drives the four RPC-gated rungs in their no-fork state and returns the /// unverified-gap count, so each counted site is pinned individually (a regression demoting one /// back to a designed skip fails this). `rolesJson` must declare `.roles.token` so the roles rung /// reaches its RPC gate rather than the designed no-roles skip. Not used by any production path. function rpcGatedSkipsForTest(string memory rolesJson) public returns (uint256 skipsOut) { + _checkOnChainCode("zz-tt-rpcgaps", "{}"); _reconcilePoolWithTar(address(1), address(1), address(1)); _checkRoles("zz-tt-rpcgaps", rolesJson, false); _checkLanesOnChain("zz-tt-rpcgaps", "", "{}"); @@ -1475,6 +1482,13 @@ contract VerifyChain is Script { return s_skips; } + /// @notice Test hook: runs the on-chain rung with no fork and then the verdict, so a test can + /// prove that rung reports its gap instead of vanishing. Not used by any production path. + function onChainNoForkVerdictForTest(string memory name) public { + _checkOnChainCode(name, "{}"); + _verdict(name); + } + /// @notice Test hook: seeds the outcome counters and runs `_verdict`, so the three-outcome /// contract (VERIFIED / INCOMPLETE / FAILED, and that designed skips never taint the verdict) is /// pinnable without the ffi/API/RPC doctor run. Not used by any production path. diff --git a/script/config/test-tooling.sh b/script/config/test-tooling.sh index 62a5128..cd15c54 100755 --- a/script/config/test-tooling.sh +++ b/script/config/test-tooling.sh @@ -1443,6 +1443,12 @@ run_case "doctor lanes rung prints its SKIP without an RPC (run INCOMPLETE)" non "lanes: on-chain reconciliation needs an RPC" -- \ env FOUNDRY_PROFILE=sync forge script script/config/VerifyChain.s.sol --tc VerifyChain --sig "run(string)" "$TMP_CHAIN" +# 28c. same for the ON-CHAIN rung (code at the ccip{} addresses). It used to return silently without a +# fork, so the verdict counted a rung that printed nothing at all. +run_case "doctor on-chain rung prints its SKIP without an RPC (run INCOMPLETE)" nonzero \ + "\[SKIP\] UNVERIFIED on-chain: code checks need an RPC" -- \ + env FOUNDRY_PROFILE=sync forge script script/config/VerifyChain.s.sol --tc VerifyChain --sig "run(string)" "$TMP_CHAIN" + # 29. BOTH=1 writes the reciprocal entry on the remote's file in the same invocation rm_fixture_config "$TMP_FILE" "$TMP_FILE_B" python3 -c " diff --git a/test/config/VerifyChainVerdict.t.sol b/test/config/VerifyChainVerdict.t.sol index a5e467d..31336da 100644 --- a/test/config/VerifyChainVerdict.t.sol +++ b/test/config/VerifyChainVerdict.t.sol @@ -73,7 +73,17 @@ contract VerifyChainVerdictTest is Test { function test_RpcGatedRungs_EachCountsOneUnverifiedGap() public { uint256 skips = new VerifyChain() .rpcGatedSkipsForTest('{"roles":{"token":{"address":"0x0000000000000000000000000000000000000001"}}}'); - assertEq(skips, 3, "registry TAR, roles, and lanes each count one unverified gap without an RPC"); + assertEq(skips, 4, "on-chain, registry TAR, roles, and lanes each count one gap without an RPC"); + } + + /// @dev The on-chain rung used to return silently with no fork, so a run that read nothing on-chain + /// still exited clean. It must report the gap and take the verdict to INCOMPLETE. + function test_OnChainRung_WithoutFork_IsIncompleteNotSilent() public { + try new VerifyChain().onChainNoForkVerdictForTest(NAME) { + assertTrue(false, "an unforked on-chain rung must not exit clean"); + } catch Error(string memory reason) { + _assertContains(reason, string.concat("check-chain INCOMPLETE for ", NAME)); + } } function _assertContains(string memory haystack, string memory needle) internal pure {