(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]); }