From 87aecb212262348e39a29b99aa220d15c02c1dce Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Wed, 29 Jul 2026 17:53:28 -0400 Subject: [PATCH 1/5] feat(policy): add compositePolicyChildIds view for composite child sets Composite policies (UNION/INTERSECT) had no on-chain read path for their child set. The only ways to recover it were decoding the CompositePolicyUpdated event or a raw eth_getStorageAt against slot 4. Adds a total view returning the stored child set: function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory); Semantics follow the existing read surface, which documents "Never reverts" on every getter: simple policies, built-in sentinels, unknown IDs, and malformed IDs all return an empty array rather than reverting with IncompatiblePolicyType. An empty return is unambiguous in practice because both write paths reject a set outside [2, 4], so a composite that exists always holds at least two children and there is no clear-the-list path. Naming: childPolicyIds was the first choice for symmetry with the write path, but a function of that name collides with the childPolicyIds parameter on createCompositePolicy and updateComposite, emitting solc warning 8760 in MockPolicyRegistry. compositePolicyChildIds compiles clean and keeps "composite" at the call site, which reads better given that a non-composite returns empty. The mock's well-formedness guard must precede _isComposite: that helper routes through _typeOf, whose PolicyType(uint8(...)) conversion panics with 0x21 on a type byte above INTERSECT. The Rust precompile compares raw type bytes and is safe without the guard. Generated with Claude Code Co-Authored-By: Claude --- src/interfaces/IPolicyRegistry.sol | 15 ++ test/lib/mocks/MockPolicyRegistry.sol | 7 + .../compositePolicyChildIds.t.sol | 137 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 test/unit/PolicyRegistry/compositePolicyChildIds.t.sol diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index 84003de..bf7a71a 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -259,4 +259,19 @@ interface IPolicyRegistry { /// /// @return Pending admin, or `address(0)`. function pendingPolicyAdmin(uint64 policyId) external view returns (address); + + /// @notice Returns the child-policy set of the composite `policyId`, in the order it was last + /// written. Returns an empty array for simple policies, built-in sentinels, unknown + /// IDs, and malformed IDs. Never reverts. + /// + /// @dev An empty return unambiguously means "not a composite": both write paths reject a set + /// outside `[2, 4]` with `ChildPoliciesOutsideOfRange`, so a composite that exists always + /// holds at least 2 children and there is no clear-the-list path. + /// @dev The registry preserves the caller's ordering verbatim and neither sorts nor + /// de-duplicates. + /// + /// @param policyId Policy to query. + /// + /// @return Child policy IDs, or an empty array. + function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory); } diff --git a/test/lib/mocks/MockPolicyRegistry.sol b/test/lib/mocks/MockPolicyRegistry.sol index a9974f5..73dfa6f 100644 --- a/test/lib/mocks/MockPolicyRegistry.sol +++ b/test/lib/mocks/MockPolicyRegistry.sol @@ -272,6 +272,13 @@ contract MockPolicyRegistry is IPolicyRegistry { return MockPolicyRegistryStorage.layout().pendingAdmins[policyId]; } + /// @inheritdoc IPolicyRegistry + function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory) { + if (!_isWellFormed(policyId)) return new uint64[](0); + if (!_isComposite(policyId)) return new uint64[](0); + return MockPolicyRegistryStorage.layout().children[policyId]; + } + // ============================================================ // INTERNAL HELPERS // ============================================================ diff --git a/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol new file mode 100644 index 0000000..6c5bc30 --- /dev/null +++ b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; + +import {IPolicyRegistry} from "base-std/interfaces/IPolicyRegistry.sol"; + +import {PolicyRegistryTest} from "base-std-test/lib/PolicyRegistryTest.sol"; +import {PolicyRegistryConstants} from "base-std-test/lib/mocks/MockPolicyRegistry.sol"; + +contract PolicyRegistryCompositePolicyChildIdsTest is PolicyRegistryTest { + /// @notice Verifies compositePolicyChildIds returns the set supplied at creation, in order + /// @dev The registry preserves caller ordering verbatim; it neither sorts nor de-duplicates. + function test_compositePolicyChildIds_success_returnsCreationSet() public { + uint64[] memory children = _makeSimpleChildren(3); + uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, children); + + uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); + assertEq(got.length, 3); + for (uint256 i = 0; i < children.length; ++i) { + assertEq(got[i], children[i]); + } + } + + /// @notice Verifies the getter tracks updateComposite as a full replacement + /// @dev Replacing [a,b] with [c,d] must drop a and b entirely, with no stale tail. + function test_compositePolicyChildIds_success_tracksUpdateComposite() public { + uint64[] memory initial = _makeSimpleChildren(4); + uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.INTERSECT, initial); + assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 4); + + uint64[] memory replacement = _childIds(initial[2], initial[3]); + vm.prank(admin); + policyRegistry.updateComposite(policyId, replacement); + + uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); + assertEq(got.length, 2); + assertEq(got[0], initial[2]); + assertEq(got[1], initial[3]); + } + + /// @notice Verifies the getter agrees with the CompositePolicyUpdated event payload + /// @dev Indexers reconcile logs against live reads; the two must never disagree. + function test_compositePolicyChildIds_success_matchesEmittedEvent() public { + uint64[] memory children = _makeSimpleChildren(2); + + vm.recordLogs(); + uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, children); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + int256 index = _firstLogIndex(logs, IPolicyRegistry.CompositePolicyUpdated.selector); + assertGe(index, 0, "CompositePolicyUpdated not emitted"); + // `policyId` and `updater` are indexed, so only the child array sits in `data`. + uint64[] memory emitted = abi.decode(logs[uint256(index)].data, (uint64[])); + + uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); + assertEq(got.length, emitted.length); + for (uint256 i = 0; i < got.length; ++i) { + assertEq(got[i], emitted[i]); + } + } + + /// @notice Verifies a duplicated child ID is returned as many times as supplied + /// @dev Documents that the registry does not de-duplicate; a UNION over [a,a] is legal. + function test_compositePolicyChildIds_success_preservesDuplicates() public { + uint64 child = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); + uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, _childIds(child, child)); + + uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); + assertEq(got.length, 2); + assertEq(got[0], child); + assertEq(got[1], child); + } + + /// @notice Verifies the child set survives an admin transfer and a renounce + /// @dev The set lives in its own slot, untouched by admin-lane writes to the packed word. + function test_compositePolicyChildIds_success_survivesRenounce() public { + uint64[] memory children = _makeSimpleChildren(2); + uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, children); + + vm.prank(admin); + policyRegistry.renounceAdmin(policyId); + + // Frozen forever (updateComposite now reverts Unauthorized for every caller), but still readable. + uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); + assertEq(got.length, 2); + assertEq(got[0], children[0]); + assertEq(got[1], children[1]); + } + + /// @notice Verifies compositePolicyChildIds returns empty for a simple policy + /// @dev Simple policies never have a child set; the gate byte short-circuits before any read. + function test_compositePolicyChildIds_success_emptyForSimplePolicy() public { + uint64 allowlist = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); + uint64 blocklist = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.BLOCKLIST); + assertEq(policyRegistry.compositePolicyChildIds(allowlist).length, 0); + assertEq(policyRegistry.compositePolicyChildIds(blocklist).length, 0); + } + + /// @notice Verifies compositePolicyChildIds returns empty for built-in sentinels + function test_compositePolicyChildIds_success_emptyForBuiltins() public view { + assertEq(policyRegistry.compositePolicyChildIds(PolicyRegistryConstants.ALWAYS_ALLOW_ID).length, 0); + assertEq(policyRegistry.compositePolicyChildIds(PolicyRegistryConstants.ALWAYS_BLOCK_ID).length, 0); + } + + /// @notice Verifies compositePolicyChildIds returns empty for a well-formed but uncreated id + /// @dev Lookup miss returns an empty array rather than reverting. + function test_compositePolicyChildIds_success_emptyForUncreated(uint64 seed) public view { + uint64 policyId = _wellFormedUncreatedPolicyId(seed); + assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0); + } + + /// @notice Verifies compositePolicyChildIds returns empty for a malformed id + /// @dev Malformed-ID short-circuit returns empty, matching the rest of the read surface. + function test_compositePolicyChildIds_success_emptyForMalformedId(uint64 seed) public view { + uint64 policyId = _malformedPolicyId(seed); + assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0); + } + + /// @notice Verifies an existing composite never reports fewer than MIN_CHILD_POLICIES children + /// @dev This is what makes an empty return unambiguously mean "not a composite": there is no + /// clear-the-list path, so a created composite can never decay to an empty set. + function test_compositePolicyChildIds_success_createdCompositeIsNeverEmpty() public { + uint64 policyId = _createComposite(IPolicyRegistry.PolicyType.UNION, MIN_CHILD_POLICIES); + + uint64[] memory empty = new uint64[](0); + vm.prank(admin); + vm.expectRevert( + abi.encodeWithSelector( + IPolicyRegistry.ChildPoliciesOutsideOfRange.selector, MIN_CHILD_POLICIES, MAX_CHILD_POLICIES + ) + ); + policyRegistry.updateComposite(policyId, empty); + + assertEq(policyRegistry.compositePolicyChildIds(policyId).length, MIN_CHILD_POLICIES); + } +} From de10b22a23891c83c318f7e4fbf8b73b1e7c32c4 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 30 Jul 2026 09:28:38 -0400 Subject: [PATCH 2/5] Apply suggestion from @stevieraykatz Co-authored-by: katzman --- src/interfaces/IPolicyRegistry.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index bf7a71a..b0b1164 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -260,9 +260,9 @@ interface IPolicyRegistry { /// @return Pending admin, or `address(0)`. function pendingPolicyAdmin(uint64 policyId) external view returns (address); - /// @notice Returns the child-policy set of the composite `policyId`, in the order it was last - /// written. Returns an empty array for simple policies, built-in sentinels, unknown - /// IDs, and malformed IDs. Never reverts. + /// @notice Returns the child-policy set of the composite `policyId`. + /// + /// @dev Child-policies are listed in the order they were last written. /// /// @dev An empty return unambiguously means "not a composite": both write paths reject a set /// outside `[2, 4]` with `ChildPoliciesOutsideOfRange`, so a composite that exists always From 91176b6b876e15f175a47a45841d22d17df54e11 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 30 Jul 2026 09:28:48 -0400 Subject: [PATCH 3/5] Apply suggestion from @stevieraykatz Co-authored-by: katzman --- src/interfaces/IPolicyRegistry.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index b0b1164..a3202c0 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -264,9 +264,7 @@ interface IPolicyRegistry { /// /// @dev Child-policies are listed in the order they were last written. /// - /// @dev An empty return unambiguously means "not a composite": both write paths reject a set - /// outside `[2, 4]` with `ChildPoliciesOutsideOfRange`, so a composite that exists always - /// holds at least 2 children and there is no clear-the-list path. + /// @dev An empty return unambiguously means "not a composite". /// @dev The registry preserves the caller's ordering verbatim and neither sorts nor /// de-duplicates. /// From 95282308ab1f66a34ccb0ea824694d6432572a8f Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 30 Jul 2026 09:41:09 -0400 Subject: [PATCH 4/5] test(policy): scope compositePolicyChildIds tests to the selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #183. test_..._success_createdCompositeIsNeverEmpty asserted a ChildPoliciesOutsideOfRange revert under a _success_ name, against the repo's _success_/_revert_ convention, and duplicated coverage already pinned thoroughly by updateComposite.t.sol (under-range 0 and 1, over-range 5..8) plus three other files. Scopes the file to the selector's own behavior, 10 tests -> 5: - returnsCreationSet newly created - tracksUpdateComposite exists and updated - emptyForUncreated does not exist - emptyAfterFailedCreation creation failed, nothing persisted (new) - emptyForMalformedId malformed id (regression guard) The new failed-creation test uses a bare vm.expectRevert() rather than the typed selector: the failure is setup for the read, not the assertion under test, which is what made the old test read as a revert test in disguise. It also pins that the predicted ID is the one the failed call would have taken, by showing the next valid creation claims exactly it — otherwise the empty read would pass for free on an unrelated ID. Dropped matchesEmittedEvent, preservesDuplicates, survivesRenounce, emptyForSimplePolicy, emptyForBuiltins, liveCompositeIsNeverEmpty. Guard coverage survives: emptyForUncreated fuzzes a top byte across the whole PolicyType range, so it exercises the !_isComposite branch that emptyForSimplePolicy covered, and emptyForMalformedId still covers !_isWellFormed. Also applies the two natspec suggestions from review. Generated with Claude Code Co-Authored-By: Claude --- src/interfaces/IPolicyRegistry.sol | 3 +- .../compositePolicyChildIds.t.sol | 121 +++++------------- 2 files changed, 36 insertions(+), 88 deletions(-) diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index a3202c0..6e0477c 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -263,7 +263,8 @@ interface IPolicyRegistry { /// @notice Returns the child-policy set of the composite `policyId`. /// /// @dev Child-policies are listed in the order they were last written. - /// + /// @dev Returns an empty array for simple policies, built-in sentinels, unknown IDs, and + /// malformed IDs. Never reverts. /// @dev An empty return unambiguously means "not a composite". /// @dev The registry preserves the caller's ordering verbatim and neither sorts nor /// de-duplicates. diff --git a/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol index 6c5bc30..cdb61ac 100644 --- a/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol +++ b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol @@ -1,12 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {Vm} from "forge-std/Vm.sol"; - import {IPolicyRegistry} from "base-std/interfaces/IPolicyRegistry.sol"; import {PolicyRegistryTest} from "base-std-test/lib/PolicyRegistryTest.sol"; -import {PolicyRegistryConstants} from "base-std-test/lib/mocks/MockPolicyRegistry.sol"; contract PolicyRegistryCompositePolicyChildIdsTest is PolicyRegistryTest { /// @notice Verifies compositePolicyChildIds returns the set supplied at creation, in order @@ -23,7 +20,7 @@ contract PolicyRegistryCompositePolicyChildIdsTest is PolicyRegistryTest { } /// @notice Verifies the getter tracks updateComposite as a full replacement - /// @dev Replacing [a,b] with [c,d] must drop a and b entirely, with no stale tail. + /// @dev Replacing [a,b,c,d] with [c,d] must drop a and b entirely function test_compositePolicyChildIds_success_tracksUpdateComposite() public { uint64[] memory initial = _makeSimpleChildren(4); uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.INTERSECT, initial); @@ -39,99 +36,49 @@ contract PolicyRegistryCompositePolicyChildIdsTest is PolicyRegistryTest { assertEq(got[1], initial[3]); } - /// @notice Verifies the getter agrees with the CompositePolicyUpdated event payload - /// @dev Indexers reconcile logs against live reads; the two must never disagree. - function test_compositePolicyChildIds_success_matchesEmittedEvent() public { - uint64[] memory children = _makeSimpleChildren(2); - - vm.recordLogs(); - uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, children); - - Vm.Log[] memory logs = vm.getRecordedLogs(); - int256 index = _firstLogIndex(logs, IPolicyRegistry.CompositePolicyUpdated.selector); - assertGe(index, 0, "CompositePolicyUpdated not emitted"); - // `policyId` and `updater` are indexed, so only the child array sits in `data`. - uint64[] memory emitted = abi.decode(logs[uint256(index)].data, (uint64[])); - - uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); - assertEq(got.length, emitted.length); - for (uint256 i = 0; i < got.length; ++i) { - assertEq(got[i], emitted[i]); - } - } - - /// @notice Verifies a duplicated child ID is returned as many times as supplied - /// @dev Documents that the registry does not de-duplicate; a UNION over [a,a] is legal. - function test_compositePolicyChildIds_success_preservesDuplicates() public { - uint64 child = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); - uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, _childIds(child, child)); - - uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); - assertEq(got.length, 2); - assertEq(got[0], child); - assertEq(got[1], child); - } - - /// @notice Verifies the child set survives an admin transfer and a renounce - /// @dev The set lives in its own slot, untouched by admin-lane writes to the packed word. - function test_compositePolicyChildIds_success_survivesRenounce() public { - uint64[] memory children = _makeSimpleChildren(2); - uint64 policyId = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, children); - - vm.prank(admin); - policyRegistry.renounceAdmin(policyId); - - // Frozen forever (updateComposite now reverts Unauthorized for every caller), but still readable. - uint64[] memory got = policyRegistry.compositePolicyChildIds(policyId); - assertEq(got.length, 2); - assertEq(got[0], children[0]); - assertEq(got[1], children[1]); - } - - /// @notice Verifies compositePolicyChildIds returns empty for a simple policy - /// @dev Simple policies never have a child set; the gate byte short-circuits before any read. - function test_compositePolicyChildIds_success_emptyForSimplePolicy() public { - uint64 allowlist = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); - uint64 blocklist = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.BLOCKLIST); - assertEq(policyRegistry.compositePolicyChildIds(allowlist).length, 0); - assertEq(policyRegistry.compositePolicyChildIds(blocklist).length, 0); - } - - /// @notice Verifies compositePolicyChildIds returns empty for built-in sentinels - function test_compositePolicyChildIds_success_emptyForBuiltins() public view { - assertEq(policyRegistry.compositePolicyChildIds(PolicyRegistryConstants.ALWAYS_ALLOW_ID).length, 0); - assertEq(policyRegistry.compositePolicyChildIds(PolicyRegistryConstants.ALWAYS_BLOCK_ID).length, 0); - } - /// @notice Verifies compositePolicyChildIds returns empty for a well-formed but uncreated id - /// @dev Lookup miss returns an empty array rather than reverting. + /// @dev Lookup miss returns an empty array rather than reverting. The fuzzed top byte spans + /// the whole PolicyType range, so this covers uncreated simple and composite IDs alike. function test_compositePolicyChildIds_success_emptyForUncreated(uint64 seed) public view { uint64 policyId = _wellFormedUncreatedPolicyId(seed); assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0); } + /// @notice Verifies a reverted composite creation persists no child set + /// @dev The child-count guard runs before the counter is consumed, so the ID that would have + /// been assigned stays unassigned and reads empty. Deliberately a bare `vm.expectRevert()`: + /// the typed `ChildPoliciesOutsideOfRange` revert is already pinned by + /// `createCompositePolicy.t.sol` and `createCompositePolicy_revertOrder.t.sol`, and the + /// failure here is setup for the read, not the assertion under test. + function test_compositePolicyChildIds_success_emptyAfterFailedCreation() public { + // Children first — `_makeSimpleChildren` consumes counter values of its own, so the + // prediction below has to come after them. + uint64[] memory children = _makeSimpleChildren(2); + uint64 predicted = _predictNextPolicyId(IPolicyRegistry.PolicyType.UNION); + + // One child is below MIN_CHILD_POLICIES, so creation fails. + uint64[] memory tooFew = new uint64[](1); + tooFew[0] = children[0]; + vm.expectRevert(); + policyRegistry.createCompositePolicy(admin, IPolicyRegistry.PolicyType.UNION, tooFew); + + assertEq(policyRegistry.compositePolicyChildIds(predicted).length, 0); + + // Pins that `predicted` really is the ID the failed call would have taken, rather than + // some unrelated unassigned ID that reads empty for free: the failure consumed no + // counter, so the next valid creation claims exactly it. + uint64 actual = _createComposite(admin, admin, IPolicyRegistry.PolicyType.UNION, children); + assertEq(actual, predicted); + assertEq(policyRegistry.compositePolicyChildIds(actual).length, 2); + } + /// @notice Verifies compositePolicyChildIds returns empty for a malformed id /// @dev Malformed-ID short-circuit returns empty, matching the rest of the read surface. + /// Regression guard: `_isComposite` routes through `_typeOf`, whose + /// `PolicyType(uint8(...))` conversion panics 0x21 on a type byte above INTERSECT, so + /// the mock's `_isWellFormed` check must run first. function test_compositePolicyChildIds_success_emptyForMalformedId(uint64 seed) public view { uint64 policyId = _malformedPolicyId(seed); assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0); } - - /// @notice Verifies an existing composite never reports fewer than MIN_CHILD_POLICIES children - /// @dev This is what makes an empty return unambiguously mean "not a composite": there is no - /// clear-the-list path, so a created composite can never decay to an empty set. - function test_compositePolicyChildIds_success_createdCompositeIsNeverEmpty() public { - uint64 policyId = _createComposite(IPolicyRegistry.PolicyType.UNION, MIN_CHILD_POLICIES); - - uint64[] memory empty = new uint64[](0); - vm.prank(admin); - vm.expectRevert( - abi.encodeWithSelector( - IPolicyRegistry.ChildPoliciesOutsideOfRange.selector, MIN_CHILD_POLICIES, MAX_CHILD_POLICIES - ) - ); - policyRegistry.updateComposite(policyId, empty); - - assertEq(policyRegistry.compositePolicyChildIds(policyId).length, MIN_CHILD_POLICIES); - } } From d99c34f56e902be6a2298baf16edab5bcaf39f8a Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Thu, 30 Jul 2026 09:44:55 -0400 Subject: [PATCH 5/5] chore: update composite polic --- .../PolicyRegistry/compositePolicyChildIds.t.sol | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol index cdb61ac..a135e0e 100644 --- a/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol +++ b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol @@ -37,19 +37,13 @@ contract PolicyRegistryCompositePolicyChildIdsTest is PolicyRegistryTest { } /// @notice Verifies compositePolicyChildIds returns empty for a well-formed but uncreated id - /// @dev Lookup miss returns an empty array rather than reverting. The fuzzed top byte spans - /// the whole PolicyType range, so this covers uncreated simple and composite IDs alike. + /// @dev Lookup should return for an uncreated ID. function test_compositePolicyChildIds_success_emptyForUncreated(uint64 seed) public view { uint64 policyId = _wellFormedUncreatedPolicyId(seed); assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0); } - /// @notice Verifies a reverted composite creation persists no child set - /// @dev The child-count guard runs before the counter is consumed, so the ID that would have - /// been assigned stays unassigned and reads empty. Deliberately a bare `vm.expectRevert()`: - /// the typed `ChildPoliciesOutsideOfRange` revert is already pinned by - /// `createCompositePolicy.t.sol` and `createCompositePolicy_revertOrder.t.sol`, and the - /// failure here is setup for the read, not the assertion under test. + /// @notice Verifies a composite creation that fails persists no child set.. function test_compositePolicyChildIds_success_emptyAfterFailedCreation() public { // Children first — `_makeSimpleChildren` consumes counter values of its own, so the // prediction below has to come after them. @@ -73,10 +67,7 @@ contract PolicyRegistryCompositePolicyChildIdsTest is PolicyRegistryTest { } /// @notice Verifies compositePolicyChildIds returns empty for a malformed id - /// @dev Malformed-ID short-circuit returns empty, matching the rest of the read surface. - /// Regression guard: `_isComposite` routes through `_typeOf`, whose - /// `PolicyType(uint8(...))` conversion panics 0x21 on a type byte above INTERSECT, so - /// the mock's `_isWellFormed` check must run first. + /// @dev Malformed-ID short-circuit returns empty. function test_compositePolicyChildIds_success_emptyForMalformedId(uint64 seed) public view { uint64 policyId = _malformedPolicyId(seed); assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0);