app(CanonicalControlCatalog::class)->listPayload(), ]; expect($payload['controls'])->not->toBeEmpty(); foreach ($payload['controls'] as $control) { expect($control)->toHaveKeys([ 'control_key', 'name', 'domain_key', 'subdomain_key', 'control_class', 'summary', 'operator_description', 'detectability_class', 'evaluation_strategy', 'evidence_archetypes', 'artifact_suitability', 'historical_status', ])->and($control['artifact_suitability'])->toHaveKeys([ 'baseline', 'drift', 'finding', 'exception', 'evidence', 'review', 'report', ]); } }); it('returns resolved, unresolved, and ambiguous resolution shapes without guessing', function (): void { $resolver = app(CanonicalControlResolver::class); $resolved = $resolver->resolve(new CanonicalControlResolutionRequest( provider: 'microsoft', consumerContext: 'review', subjectFamilyKey: 'entra_admin_roles', workload: 'entra', signalKey: 'entra_admin_roles.global_admin_assignment', ))->toArray(); $unresolved = $resolver->resolve(new CanonicalControlResolutionRequest( provider: 'microsoft', consumerContext: 'review', subjectFamilyKey: 'not_bound', ))->toArray(); $ambiguous = $resolver->resolve(new CanonicalControlResolutionRequest( provider: 'microsoft', consumerContext: 'review', subjectFamilyKey: 'conditional_access_policy', workload: 'entra', ))->toArray(); expect($resolved)->toHaveKeys(['status', 'control']) ->and($resolved['status'])->toBe('resolved') ->and($resolved['control']['control_key'])->toBe('privileged_access_governance') ->and($unresolved)->toHaveKeys(['status', 'reason_code', 'binding_context']) ->and($unresolved['reason_code'])->toBe('missing_binding') ->and($ambiguous)->toHaveKeys(['status', 'reason_code', 'candidate_control_keys', 'binding_context']) ->and($ambiguous['status'])->toBe('ambiguous') ->and($ambiguous['candidate_control_keys'])->toEqualCanonicalizing([ 'conditional_access_enforcement', 'strong_authentication', ]); });