diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index 84003de..6e0477c 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -259,4 +259,18 @@ 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`. + /// + /// @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. + /// + /// @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..a135e0e --- /dev/null +++ b/test/unit/PolicyRegistry/compositePolicyChildIds.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IPolicyRegistry} from "base-std/interfaces/IPolicyRegistry.sol"; + +import {PolicyRegistryTest} from "base-std-test/lib/PolicyRegistryTest.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,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); + 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 compositePolicyChildIds returns empty for a well-formed but uncreated id + /// @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 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. + 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. + function test_compositePolicyChildIds_success_emptyForMalformedId(uint64 seed) public view { + uint64 policyId = _malformedPolicyId(seed); + assertEq(policyRegistry.compositePolicyChildIds(policyId).length, 0); + } +}