TenantAtlas/apps/platform/tests/Feature/Filament/BaselineSubjectResolutionPageTest.php
ahmido 39298f27f2 feat(ui): implement baseline subject resolution ui (#455)
Added `BaselineSubjectResolution` page and supporting logic to visualize missing identities, ambiguous matches, and skipped coverages as defined in Spec 384. Replaces legacy compare warnings with an actionable, deterministic UI surface.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #455
2026-06-16 23:36:38 +00:00

371 lines
16 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\BaselineSubjectResolution;
use App\Models\AuditLog;
use App\Models\ProviderResourceBinding;
use App\Models\WorkspaceMembership;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Baselines\BaselineSubjectResolutionQuery;
use App\Support\Audit\AuditActionId;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Navigation\CrossResourceNavigationMatrix;
use App\Support\Navigation\RelatedNavigationResolver;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Resources\ProviderResourceBindingStatus;
use App\Support\Resources\ProviderResourceDescriptor;
use App\Support\Resources\ProviderResourceResolutionMode;
use App\Support\Resources\ResourceIdentity;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures;
uses(RefreshDatabase::class);
it('renders an environment-scoped actionable subject worklist', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedSubjectResolutionRun($tenant);
spec384BaselineSubjectResolutionLivewire($tenant, $user, ['operation_run_id' => (int) $run->getKey()])
->assertOk()
->assertSee('Baseline subject resolution')
->assertSee('Duplicate policy')
->assertSee('Unresolved Duplicate Candidates')
->assertSee('Binding required')
->assertSee('TenantPilot-only');
});
it('renders specific empty states for missing compare runs and quiet compare results', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
spec384BaselineSubjectResolutionLivewire($tenant, $user)
->assertOk()
->assertSee('Run baseline compare first')
->assertSee('No baseline compare run exists for this environment yet.');
[$quietUser, $quietTenant] = createUserWithTenant(role: 'owner');
[$profile, $snapshot] = seedActiveBaselineForTenant($quietTenant);
$quietRun = seedBaselineCompareRun($quietTenant, $profile, $snapshot, [
'result_semantics' => [
'version' => 1,
'subject_outcomes' => [
BaselineSubjectResolutionFixtures::semanticOutcome([
'reason' => 'verified_no_drift',
'actionability' => 'none',
'readiness_impact' => 'no_impact',
'subject' => ['subject_type_key' => 'deviceConfiguration', 'subject_key' => 'quiet'],
]),
],
],
]);
spec384BaselineSubjectResolutionLivewire($quietTenant, $quietUser, ['operation_run_id' => (int) $quietRun->getKey()])
->assertOk()
->assertSee('No baseline subject decisions required')
->assertSee('The selected compare context has no unresolved or decision-required baseline subjects.');
});
it('records manual bindings through the confirmed Filament action', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedSubjectResolutionRun($tenant);
$row = app(BaselineSubjectResolutionQuery::class)->rows($tenant, [
'operation_run_id' => (int) $run->getKey(),
])[0];
$candidateKey = (string) $row['candidates'][0]['candidate_key'];
$component = spec384BaselineSubjectResolutionLivewire($tenant, $user, ['operation_run_id' => (int) $run->getKey()]);
$record = collect($component->instance()->getTableRecords()->items())->first();
$component->callTableAction('bindSubject', $record, data: [
'candidate_key' => $candidateKey,
'operator_note' => 'Operator selected the matching provider resource after reviewing duplicate candidates.',
]);
$binding = ProviderResourceBinding::query()
->where('managed_environment_id', (int) $tenant->getKey())
->firstOrFail();
expect($binding->resolution_mode)->toBe(ProviderResourceResolutionMode::ManualBinding)
->and($binding->provider_resource_id)->toBe('candidate-left')
->and((int) $binding->source_operation_run_id)->toBe((int) $run->getKey());
expect(AuditLog::query()
->where('action', AuditActionId::ProviderResourceBindingCreated->value)
->where('managed_environment_id', (int) $tenant->getKey())
->exists())->toBeTrue();
});
it('records and revokes subject decisions through confirmed Filament actions', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedDecisionIdentityRun($tenant);
$component = spec384BaselineSubjectResolutionLivewire($tenant, $user, ['operation_run_id' => (int) $run->getKey()]);
$record = collect($component->instance()->getTableRecords()->items())->first();
$component->callTableAction('recordDecision', $record, data: [
'decision' => 'accepted_limitation',
'operator_note' => 'Operator accepted this baseline limitation after confirming provider coverage scope.',
]);
$binding = ProviderResourceBinding::query()
->where('managed_environment_id', (int) $tenant->getKey())
->firstOrFail();
expect($binding->resolution_mode)->toBe(ProviderResourceResolutionMode::AcceptedLimitation)
->and($binding->binding_status)->toBe(ProviderResourceBindingStatus::Active)
->and(AuditLog::query()
->where('action', AuditActionId::ProviderResourceBindingCreated->value)
->where('managed_environment_id', (int) $tenant->getKey())
->exists())->toBeTrue();
$updatedRecord = collect($component->instance()->getTableRecords()->items())->first();
$component
->assertTableActionVisible('revokeDecision', $updatedRecord)
->callTableAction('revokeDecision', $updatedRecord, data: [
'operator_note' => 'Operator revoked the limitation because fresh provider evidence is expected.',
]);
expect($binding->refresh()->binding_status)->toBe(ProviderResourceBindingStatus::Revoked)
->and(AuditLog::query()
->where('action', AuditActionId::ProviderResourceBindingRevoked->value)
->where('managed_environment_id', (int) $tenant->getKey())
->exists())->toBeTrue();
});
it('disables decision actions for workspace members missing manage capability', function (): void {
[$owner, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedSubjectResolutionRun($tenant);
[$readonly] = createUserWithTenant(tenant: $tenant, role: 'owner', workspaceRole: 'readonly');
$component = spec384BaselineSubjectResolutionLivewire($tenant, $readonly, ['operation_run_id' => (int) $run->getKey()]);
$record = collect($component->instance()->getTableRecords()->items())->first();
$component
->assertTableActionVisible('bindSubject', $record)
->assertTableActionDisabled('bindSubject', $record);
});
it('returns not found for users outside the workspace/environment scope', function (): void {
[$owner, $tenant] = createUserWithTenant(role: 'owner');
spec384SeedSubjectResolutionRun($tenant);
[$outsider] = createUserWithTenant(role: 'owner');
$this->actingAs($outsider)
->get(ManagedEnvironmentLinks::baselineSubjectResolutionUrl($tenant))
->assertNotFound();
});
it('returns not found when the route workspace does not own the environment', function (): void {
[$owner, $tenant] = createUserWithTenant(role: 'owner');
spec384SeedSubjectResolutionRun($tenant);
[, $foreignTenant] = createUserWithTenant(role: 'owner');
$url = str_replace(
'/workspaces/'.$tenant->workspace->slug.'/',
'/workspaces/'.$foreignTenant->workspace->slug.'/',
ManagedEnvironmentLinks::baselineSubjectResolutionUrl($tenant),
);
$this->actingAs($owner)
->get($url)
->assertNotFound();
});
it('reauthorizes livewire reads after workspace membership changes', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedSubjectResolutionRun($tenant);
$component = spec384BaselineSubjectResolutionLivewire($tenant, $user, ['operation_run_id' => (int) $run->getKey()])
->assertOk();
WorkspaceMembership::query()
->where('workspace_id', (int) $tenant->workspace_id)
->where('user_id', (int) $user->getKey())
->delete();
app(WorkspaceCapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
expect(fn (): mixed => $component->instance()->currentEnvironment())
->toThrow(NotFoundHttpException::class);
});
it('adds a contextual link from baseline compare only when action is required', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedSubjectResolutionRun($tenant);
baselineCompareLandingLivewire($tenant, user: $user)
->assertSee('Resolve baseline subjects')
->assertSee('1 subject need identity or coverage decisions');
[$quietUser, $quietTenant] = createUserWithTenant(role: 'owner');
[$profile, $snapshot] = seedActiveBaselineForTenant($quietTenant);
seedBaselineCompareRun($quietTenant, $profile, $snapshot, [
'result_semantics' => [
'version' => 1,
'subject_outcomes' => [
BaselineSubjectResolutionFixtures::semanticOutcome([
'reason' => 'verified_no_drift',
'actionability' => 'none',
'readiness_impact' => 'no_impact',
'subject' => ['subject_type_key' => 'deviceConfiguration', 'subject_key' => 'quiet'],
]),
],
],
]);
baselineCompareLandingLivewire($quietTenant, user: $quietUser)
->assertDontSee('Resolve baseline subjects');
});
it('adds a baseline subject resolution link to baseline compare run related links', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = spec384SeedSubjectResolutionRun($tenant);
$links = \App\Support\OperationRunLinks::related($run, $tenant);
expect($links)->toHaveKey('Baseline Subject Resolution')
->and($links['Baseline Subject Resolution'])->toContain('baseline-subject-resolution')
->and($links['Baseline Subject Resolution'])->toContain('operation_run_id='.(int) $run->getKey());
[, $quietTenant] = createUserWithTenant(role: 'owner');
[$profile, $snapshot] = seedActiveBaselineForTenant($quietTenant);
$quietRun = seedBaselineCompareRun($quietTenant, $profile, $snapshot, [
'result_semantics' => [
'version' => 1,
'subject_outcomes' => [
BaselineSubjectResolutionFixtures::semanticOutcome([
'reason' => 'verified_no_drift',
'actionability' => 'none',
'readiness_impact' => 'no_impact',
'subject' => ['subject_type_key' => 'deviceConfiguration', 'subject_key' => 'quiet'],
]),
],
],
]);
expect(\App\Support\OperationRunLinks::related($quietRun, $quietTenant))
->not->toHaveKey('Baseline Subject Resolution');
});
it('adds a baseline subject resolution entry to operation run detail related navigation', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
$run = spec384SeedSubjectResolutionRun($tenant);
$entry = collect(app(RelatedNavigationResolver::class)
->detailEntries(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $run))
->firstWhere('key', 'baseline_subject_resolution');
expect($entry)->not->toBeNull()
->and($entry['targetUrl'])->toContain('baseline-subject-resolution')
->and($entry['targetUrl'])->toContain('operation_run_id='.(int) $run->getKey())
->and($entry['actionLabel'])->toBe('Resolve baseline subjects');
});
function spec384SeedSubjectResolutionRun($tenant)
{
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
$leftIdentity = ResourceIdentity::providerResource('fake-provider', 'policy', 'candidate-left');
$rightIdentity = ResourceIdentity::providerResource('fake-provider', 'policy', 'candidate-right');
$leftDescriptor = BaselineSubjectResolutionFixtures::providerDescriptor($leftIdentity, 'Duplicate policy');
$rightDescriptor = BaselineSubjectResolutionFixtures::providerDescriptor($rightIdentity, 'Duplicate policy');
BaselineSubjectResolutionFixtures::inventoryCandidate($tenant, $leftIdentity, 'Duplicate policy');
BaselineSubjectResolutionFixtures::inventoryCandidate($tenant, $rightIdentity, 'Duplicate policy');
return seedBaselineCompareRun(
tenant: $tenant,
profile: $profile,
snapshot: $snapshot,
compareContext: [
'result_semantics' => [
'version' => 1,
'subject_outcomes' => [
BaselineSubjectResolutionFixtures::semanticOutcome([
'reason' => 'unresolved_duplicate_candidates',
'actionability' => 'binding_required',
'readiness_impact' => 'customer_blocker',
'subject' => [
'subject_domain' => 'baseline',
'subject_class' => \App\Support\Baselines\SubjectClass::PolicyBacked->value,
'subject_type_key' => 'deviceConfiguration',
'subject_key' => 'legacy-display-key',
'display_label' => 'Duplicate policy',
'candidate_descriptors' => [
$leftDescriptor->toArray(),
$rightDescriptor->toArray(),
],
],
]),
],
],
],
status: OperationRunStatus::Completed->value,
outcome: OperationRunOutcome::PartiallySucceeded->value,
);
}
function spec384SeedDecisionIdentityRun($tenant)
{
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
$identity = ResourceIdentity::providerResource('fake-provider', 'policy', 'limited-policy');
$descriptor = ProviderResourceDescriptor::fromIdentity(
identity: $identity,
subjectDomain: 'baseline',
subjectClass: \App\Support\Baselines\SubjectClass::PolicyBacked,
subjectTypeKey: 'deviceConfiguration',
displayLabel: 'Accepted limitation policy',
sourceReferences: [],
fingerprint: $identity->fingerprint(),
lastSeenAt: now()->toIso8601String(),
);
return seedBaselineCompareRun(
tenant: $tenant,
profile: $profile,
snapshot: $snapshot,
compareContext: [
'result_semantics' => [
'version' => 1,
'subject_outcomes' => [
BaselineSubjectResolutionFixtures::semanticOutcome([
'reason' => 'foundation_limitation',
'actionability' => 'decision_required',
'readiness_impact' => 'internal_blocker',
'subject' => [
'subject_domain' => 'baseline',
'subject_class' => \App\Support\Baselines\SubjectClass::PolicyBacked->value,
'subject_type_key' => 'deviceConfiguration',
'subject_key' => 'accepted-limitation-policy',
'display_label' => 'Accepted limitation policy',
'provider_resource_descriptor' => $descriptor->toArray(),
],
]),
],
],
],
status: OperationRunStatus::Completed->value,
outcome: OperationRunOutcome::PartiallySucceeded->value,
);
}
function spec384BaselineSubjectResolutionLivewire($tenant, $user, array $queryParams = [])
{
$manager = Livewire::withHeaders([
'Referer' => ManagedEnvironmentLinks::baselineSubjectResolutionUrl($tenant),
])->actingAs($user);
if ($queryParams !== []) {
$manager = $manager->withQueryParams($queryParams);
}
return $manager->test(BaselineSubjectResolution::class, ['environment' => $tenant]);
}