describeForCompare('deviceConfiguration', 'policy-1', 'deviceconfiguration|policy-1'); $foundationDescriptor = $resolver->describeForCapture('roleScopeTag', 'scope-tag-1', 'rolescopetag|baseline'); $rbacDescriptor = $resolver->describeForCompare('intuneRoleDefinition', 'role-def-1', 'rbac-role'); expect($policyDescriptor->subjectClass)->toBe(SubjectClass::PolicyBacked) ->and($policyDescriptor->resolutionPath)->toBe(ResolutionPath::Policy) ->and($policyDescriptor->supportMode)->toBe('supported') ->and($policyDescriptor->sourceModelExpected)->toBe('policy'); expect($foundationDescriptor->subjectClass)->toBe(SubjectClass::FoundationBacked) ->and($foundationDescriptor->resolutionPath)->toBe(ResolutionPath::FoundationInventory) ->and($foundationDescriptor->supportMode)->toBe('limited') ->and($foundationDescriptor->sourceModelExpected)->toBe('inventory'); expect($rbacDescriptor->subjectClass)->toBe(SubjectClass::FoundationBacked) ->and($rbacDescriptor->resolutionPath)->toBe(ResolutionPath::FoundationPolicy) ->and($rbacDescriptor->supportMode)->toBe('supported') ->and($rbacDescriptor->sourceModelExpected)->toBe('policy'); }); it('maps structural and operational outcomes without flattening them into policy_not_found', function (): void { $resolver = app(SubjectResolver::class); $foundationDescriptor = $resolver->describeForCapture('notificationMessageTemplate', 'template-1', 'template-subject'); $policyDescriptor = $resolver->describeForCompare('deviceConfiguration', 'policy-1', 'policy-subject'); $structuralOutcome = $resolver->structuralInventoryOnly($foundationDescriptor); $missingPolicyOutcome = $resolver->missingExpectedRecord($policyDescriptor); $throttledOutcome = $resolver->throttled($policyDescriptor); expect($structuralOutcome->resolutionOutcome)->toBe(ResolutionOutcome::FoundationInventoryOnly) ->and($structuralOutcome->reasonCode)->toBe('foundation_not_policy_backed') ->and($structuralOutcome->operatorActionCategory)->toBe(OperatorActionCategory::ProductFollowUp) ->and($structuralOutcome->structural)->toBeTrue(); expect($missingPolicyOutcome->resolutionOutcome)->toBe(ResolutionOutcome::PolicyRecordMissing) ->and($missingPolicyOutcome->reasonCode)->toBe('policy_record_missing') ->and($missingPolicyOutcome->operatorActionCategory)->toBe(OperatorActionCategory::RunPolicySyncOrBackup) ->and($missingPolicyOutcome->structural)->toBeFalse(); expect($throttledOutcome->resolutionOutcome)->toBe(ResolutionOutcome::Throttled) ->and($throttledOutcome->retryable)->toBeTrue() ->and($throttledOutcome->operatorActionCategory)->toBe(OperatorActionCategory::Retry); }); it('guards unsupported or invalid support declarations before runtime work starts', function (): void { $guard = app(BaselineSupportCapabilityGuard::class); config()->set('tenantpilot.foundation_types', [ [ 'type' => 'intuneRoleAssignment', 'label' => 'Intune RBAC Role Assignment', 'baseline_compare' => [ 'supported' => false, 'identity_strategy' => 'external_id', ], ], [ 'type' => 'brokenFoundation', 'label' => 'Broken Foundation', 'baseline_compare' => [ 'supported' => true, 'resolution' => [ 'subject_class' => SubjectClass::FoundationBacked->value, 'resolution_path' => 'broken', 'compare_capability' => 'supported', 'capture_capability' => 'supported', 'source_model_expected' => 'inventory', ], ], ], ]); $result = $guard->guardTypes(['intuneRoleAssignment', 'brokenFoundation'], 'compare'); expect($result['allowed_types'])->toBe([]) ->and($result['unsupported_types'])->toBe(['brokenFoundation', 'intuneRoleAssignment']) ->and($result['invalid_support_types'])->toBe(['brokenFoundation']) ->and(data_get($result, 'capabilities.brokenFoundation.support_mode'))->toBe('invalid_support_config') ->and(data_get($result, 'capabilities.intuneRoleAssignment.support_mode'))->toBe('excluded'); });