makePortfolioTriageActor('Anchor ManagedEnvironment'); $targetEnvironment = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target ManagedEnvironment'); $backupSet = $this->seedPortfolioBackupConcern($targetEnvironment, TenantBackupHealthAssessment::POSTURE_STALE); $this->seedPortfolioRecoveryConcern($targetEnvironment, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $backupSet); $triageState = $this->portfolioReturnFilters( [TenantBackupHealthAssessment::POSTURE_STALE], [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED], [], TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, ); $expectedUrl = ManagedEnvironmentResource::crossEnvironmentCompareOpenUrl($targetEnvironment, $triageState); $this->portfolioTriageRegistryList($user, $anchorTenant, $triageState) ->assertTableActionVisible('compareEnvironments', $targetEnvironment) ->assertTableActionHasUrl('compareEnvironments', $expectedUrl, $targetEnvironment); $query = crossEnvironmentCompareLaunchQuery($expectedUrl); $backUrl = urldecode((string) data_get($query, 'nav.back_url')); expect($query)->toMatchArray([ 'target_environment_id' => (string) $targetEnvironment->getKey(), ]) ->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry') ->and(data_get($query, 'nav.environment_id'))->toBe((string) $targetEnvironment->getKey()) ->and(data_get($query, 'nav.back_label'))->toBe(__('localization.shell.back_to_environment_registry')) ->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE) ->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED) ->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); Livewire::withQueryParams($query) ->actingAs($user) ->test(CrossEnvironmentComparePage::class) ->assertSet('sourceEnvironmentId', null) ->assertSet('targetEnvironmentId', (string) $targetEnvironment->getKey()) ->assertActionVisible('return_to_origin') ->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === __('localization.shell.back_to_environment_registry') && $action->getUrl() === ManagedEnvironmentResource::getUrl(panel: 'admin', parameters: $triageState)); }); it('launches cross-environment compare from an exact-two bulk selection with both tenants prefilled', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor ManagedEnvironment'); $targetEnvironment = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target ManagedEnvironment'); $anchorBackupSet = $this->seedPortfolioBackupConcern($anchorTenant, TenantBackupHealthAssessment::POSTURE_STALE); $this->seedPortfolioRecoveryConcern($anchorTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $anchorBackupSet); $backupSet = $this->seedPortfolioBackupConcern($targetEnvironment, TenantBackupHealthAssessment::POSTURE_STALE); $this->seedPortfolioRecoveryConcern($targetEnvironment, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $backupSet); $triageState = $this->portfolioReturnFilters( [TenantBackupHealthAssessment::POSTURE_STALE], [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED], [], TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, ); $expectedUrl = ManagedEnvironmentResource::crossEnvironmentCompareOpenUrlForSelection( targetEnvironment: $targetEnvironment, triageState: $triageState, sourceEnvironment: $anchorTenant, ); $this->portfolioTriageRegistryList($user, $anchorTenant, $triageState) ->selectTableRecords([$anchorTenant, $targetEnvironment]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant, $targetEnvironment]) ->assertRedirect($expectedUrl); $query = crossEnvironmentCompareLaunchQuery($expectedUrl); $backUrl = urldecode((string) data_get($query, 'nav.back_url')); expect($query)->toMatchArray([ 'source_environment_id' => (string) $anchorTenant->getKey(), 'target_environment_id' => (string) $targetEnvironment->getKey(), ]) ->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry') ->and(data_get($query, 'nav.back_label'))->toBe(__('localization.shell.back_to_environment_registry')) ->and(data_get($query, 'nav.managed_environment_id'))->toBeNull() ->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE) ->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED) ->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); Livewire::withQueryParams($query) ->actingAs($user) ->test(CrossEnvironmentComparePage::class) ->assertSet('sourceEnvironmentId', (string) $anchorTenant->getKey()) ->assertSet('targetEnvironmentId', (string) $targetEnvironment->getKey()) ->assertActionVisible('return_to_origin'); }); it('keeps launch context after queueing promotion from an exact-two registry launch', function (): void { Queue::fake(); [$user, $anchorTenant] = $this->makePortfolioTriageActor( tenantName: 'Anchor ManagedEnvironment', workspaceRole: 'owner', ); $targetEnvironment = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target ManagedEnvironment'); createMinimalUserWithTenant( tenant: $targetEnvironment, user: $user, role: 'owner', workspaceRole: 'owner', ); $this->createPortfolioCompareSubject( tenant: $anchorTenant, displayName: 'Queued Launch Context Policy', snapshot: ['settings' => [['key' => 'launch-context', 'value' => 1]]], ); $triageState = $this->portfolioReturnFilters( [TenantBackupHealthAssessment::POSTURE_STALE], [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED], [], TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, ); $expectedUrl = ManagedEnvironmentResource::crossEnvironmentCompareOpenUrlForSelection( targetEnvironment: $targetEnvironment, triageState: $triageState, sourceEnvironment: $anchorTenant, ); $expectedBackUrl = ManagedEnvironmentResource::getUrl(panel: 'admin', parameters: $triageState); $query = crossEnvironmentCompareLaunchQuery($expectedUrl); $query['policy_type'] = ['deviceConfiguration']; $this->usePortfolioTriageWorkspace($user, $anchorTenant); $component = Livewire::withQueryParams($query) ->actingAs($user) ->test(CrossEnvironmentComparePage::class) ->assertSet('sourceEnvironmentId', (string) $anchorTenant->getKey()) ->assertSet('targetEnvironmentId', (string) $targetEnvironment->getKey()) ->assertSet('selectedPolicyTypes', ['deviceConfiguration']) ->assertActionVisible('return_to_origin') ->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === __('localization.shell.back_to_environment_registry') && $action->getUrl() === $expectedBackUrl); $page = $component->instance(); $page->generatePromotionPreflight(); $page->executePromotion(); $run = OperationRun::query()->latest('id')->first(); $navigationContext = CanonicalNavigationContext::fromPayload($page->navigationContextPayload); expect($run) ->not->toBeNull() ->and($run?->type)->toBe('promotion.execute') ->and(data_get($run?->context, 'selection.sourceEnvironmentId'))->toBe((int) $anchorTenant->getKey()) ->and(data_get($run?->context, 'selection.targetEnvironmentId'))->toBe((int) $targetEnvironment->getKey()) ->and(data_get($run?->context, 'selection.policyTypes'))->toBe(['deviceConfiguration']) ->and($page->sourceEnvironmentId)->toBe((string) $anchorTenant->getKey()) ->and($page->targetEnvironmentId)->toBe((string) $targetEnvironment->getKey()) ->and($page->selectedPolicyTypes)->toBe(['deviceConfiguration']) ->and($page->navigationContextPayload)->toBe($query['nav']) ->and($navigationContext?->backLinkLabel)->toBe(__('localization.shell.back_to_environment_registry')) ->and($navigationContext?->backLinkUrl)->toBe($expectedBackUrl); Queue::assertPushed(CrossEnvironmentPromotionExecutionJob::class, function (CrossEnvironmentPromotionExecutionJob $job) use ($run): bool { return $job->getOperationRun()?->is($run); }); }); it('rejects the bulk compare action until exactly two active environments are selected', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor ManagedEnvironment'); $targetEnvironment = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target ManagedEnvironment'); $thirdTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Third ManagedEnvironment'); $this->portfolioTriageRegistryList($user, $anchorTenant) ->selectTableRecords([$anchorTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant]) ->assertNotified('Select exactly two environments to compare.'); $this->portfolioTriageRegistryList($user, $anchorTenant) ->selectTableRecords([$anchorTenant, $targetEnvironment, $thirdTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant, $targetEnvironment, $thirdTenant]) ->assertNotified('Select exactly two environments to compare.'); }); it('rejects the bulk compare action when a selected environment is not active', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor ManagedEnvironment'); $onboardingTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Onboarding ManagedEnvironment'); $onboardingTenant->forceFill([ 'status' => ManagedEnvironment::STATUS_ONBOARDING, ])->save(); $this->portfolioTriageRegistryList($user, $anchorTenant) ->selectTableRecords([$anchorTenant, $onboardingTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant, $onboardingTenant]) ->assertNotified('Only active environments can be compared.'); }); it('hides the compare launch action when workspace baseline view capability is missing', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor ManagedEnvironment'); $targetEnvironment = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target ManagedEnvironment'); $resolver = \Mockery::mock(WorkspaceCapabilityResolver::class); $resolver->shouldReceive('isMember')->andReturnTrue(); $resolver->shouldReceive('can')->andReturnFalse(); app()->instance(WorkspaceCapabilityResolver::class, $resolver); $this->portfolioTriageRegistryList($user, $anchorTenant) ->assertTableActionHidden('compareEnvironments', $targetEnvironment); }); it('hides the compare launch action when the actor lacks tenant view on the launched tenant', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor ManagedEnvironment'); $targetEnvironment = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target ManagedEnvironment'); $resolver = \Mockery::mock(CapabilityResolver::class); $resolver->shouldReceive('primeMemberships')->andReturnNull(); $resolver->shouldReceive('isMember')->andReturnTrue(); $resolver->shouldReceive('can')->andReturnUsing(function (mixed $actor, mixed $tenant, string $capability) use ($targetEnvironment): bool { if ($tenant instanceof ManagedEnvironment && (int) $tenant->getKey() === (int) $targetEnvironment->getKey() && $capability === Capabilities::TENANT_VIEW) { return false; } return true; }); app()->instance(CapabilityResolver::class, $resolver); $this->portfolioTriageRegistryList($user, $anchorTenant) ->assertTableActionHidden('compareEnvironments', $targetEnvironment); });