value, GovernanceDomainKey::PlatformFoundation->value], subjectClasses: [ GovernanceSubjectClass::Policy->value, GovernanceSubjectClass::ConfigurationResource->value, ], ), ], ), ]); $scope = BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => GovernanceDomainKey::Intune->value, 'subject_class' => GovernanceSubjectClass::Policy->value, 'subject_type_keys' => ['deviceConfiguration'], 'filters' => [], ], [ 'domain_key' => GovernanceDomainKey::PlatformFoundation->value, 'subject_class' => GovernanceSubjectClass::ConfigurationResource->value, 'subject_type_keys' => ['assignmentFilter'], 'filters' => [], ], ], ]); $selection = $registry->select($scope); expect($selection->isSupported())->toBeTrue() ->and($selection->strategyKey?->value)->toBe('intune_policy') ->and($selection->matchedScopeEntries)->toHaveCount(2) ->and($selection->rejectedScopeEntries)->toBe([]); }); it('rejects canonical scope entries when no strategy supports them', function (): void { $registry = new CompareStrategyRegistry([ compareStrategyStub( key: 'intune_policy', capabilities: [ new CompareStrategyCapability( strategyKey: CompareStrategyKey::intunePolicy(), domainKeys: [GovernanceDomainKey::Intune->value], subjectClasses: [GovernanceSubjectClass::Policy->value], ), ], ), ]); $scope = new BaselineScope( entries: [[ 'domain_key' => GovernanceDomainKey::Entra->value, 'subject_class' => GovernanceSubjectClass::Control->value, 'subject_type_keys' => ['conditionalAccessPolicy'], 'filters' => [], ]], version: 2, ); $selection = $registry->select($scope); expect($selection->isUnsupported())->toBeTrue() ->and($selection->strategyKey)->toBeNull() ->and($selection->matchedScopeEntries)->toBe([]) ->and($selection->rejectedScopeEntries)->toHaveCount(1); }); it('marks scope as mixed when multiple strategy families are required', function (): void { $registry = new CompareStrategyRegistry([ compareStrategyStub( key: 'intune_policy', capabilities: [ new CompareStrategyCapability( strategyKey: CompareStrategyKey::intunePolicy(), domainKeys: [GovernanceDomainKey::Intune->value], subjectClasses: [GovernanceSubjectClass::Policy->value], ), ], ), compareStrategyStub( key: 'future_control', capabilities: [ new CompareStrategyCapability( strategyKey: CompareStrategyKey::from('future_control'), domainKeys: [GovernanceDomainKey::Entra->value], subjectClasses: [GovernanceSubjectClass::Control->value], ), ], ), ]); $scope = new BaselineScope( entries: [ [ 'domain_key' => GovernanceDomainKey::Intune->value, 'subject_class' => GovernanceSubjectClass::Policy->value, 'subject_type_keys' => ['deviceConfiguration'], 'filters' => [], ], [ 'domain_key' => GovernanceDomainKey::Entra->value, 'subject_class' => GovernanceSubjectClass::Control->value, 'subject_type_keys' => ['conditionalAccessPolicy'], 'filters' => [], ], ], version: 2, ); $selection = $registry->select($scope); expect($selection->isMixed())->toBeTrue() ->and($selection->strategyKey)->toBeNull() ->and($selection->matchedScopeEntries)->toHaveCount(2) ->and($selection->diagnostics['matched_strategy_keys'] ?? [])->toEqual(['future_control', 'intune_policy']); }); it('supports deterministic future-domain selection without implicit intune fallback', function (): void { $registry = new CompareStrategyRegistry([ compareStrategyStub( key: 'future_control', capabilities: [ new CompareStrategyCapability( strategyKey: CompareStrategyKey::from('future_control'), domainKeys: [GovernanceDomainKey::Entra->value], subjectClasses: [GovernanceSubjectClass::Control->value], ), ], ), ]); $scope = new BaselineScope( entries: [[ 'domain_key' => GovernanceDomainKey::Entra->value, 'subject_class' => GovernanceSubjectClass::Control->value, 'subject_type_keys' => ['conditionalAccessPolicy'], 'filters' => [], ]], version: 2, ); $selection = $registry->select($scope); expect($selection->isSupported())->toBeTrue() ->and($selection->strategyKey?->value)->toBe('future_control') ->and($registry->resolve('future_control'))->toBeInstanceOf(CompareStrategy::class); }); it('throws when resolving an unknown strategy key', function (): void { $registry = new CompareStrategyRegistry([]); expect(fn (): CompareStrategy => $registry->resolve('missing_strategy')) ->toThrow(InvalidArgumentException::class, 'Unknown compare strategy'); }); /** * @param list $capabilities */ function compareStrategyStub(string $key, array $capabilities): CompareStrategy { return new class($key, $capabilities) implements CompareStrategy { /** * @param list $capabilities */ public function __construct( private readonly string $keyValue, private readonly array $capabilities, ) {} public function key(): CompareStrategyKey { return CompareStrategyKey::from($this->keyValue); } public function capabilities(): array { return $this->capabilities; } public function compare( CompareOrchestrationContext $context, Tenant $tenant, array $baselineItems, array $currentItems, array $resolvedCurrentEvidence, array $severityMapping, ): array { return [ 'subject_results' => [], 'diagnostics' => [], ]; } }; }