TenantAtlas/tests/Unit/Support/Baselines/SubjectResolverTest.php
ahmido c17255f854 feat: implement baseline subject resolution semantics (#193)
## Summary
- add the structured subject-resolution foundation for baseline compare and baseline capture, including capability guards, subject descriptors, resolution outcomes, and operator action categories
- persist structured evidence-gap subject records and update compare/capture surfaces, landing projections, and cleanup tooling to use the new contract
- add Spec 163 artifacts and focused Pest coverage for classification, determinism, cleanup, and DB-only rendering

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Unit/Support/Baselines tests/Feature/Baselines tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php`

## Notes
- verified locally that a fresh post-restart baseline compare run now writes structured `baseline_compare.evidence_gaps.subjects` records instead of the legacy broad payload shape
- excluded the separate `docs/product/spec-candidates.md` worktree change from this branch commit and PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #193
2026-03-25 12:40:45 +00:00

96 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Support\Baselines\BaselineSupportCapabilityGuard;
use App\Support\Baselines\OperatorActionCategory;
use App\Support\Baselines\ResolutionOutcome;
use App\Support\Baselines\ResolutionPath;
use App\Support\Baselines\SubjectClass;
use App\Support\Baselines\SubjectResolver;
it('derives truthful runtime capability and descriptors for supported policy and foundation types', function (): void {
$resolver = app(SubjectResolver::class);
$policyDescriptor = $resolver->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');
});