shouldReceive('build')->andReturn([ 'overview' => array_replace_recursive([ 'overall' => 'ready', 'counts' => [ 'missing_application' => 0, 'missing_delegated' => 0, ], 'freshness' => [ 'is_stale' => false, 'last_refreshed_at' => now()->toIso8601String(), ], ], $overview), ]); }); } /** * @return array{ * review:\App\Models\EnvironmentReview, * successor:\App\Models\EnvironmentReview|null, * reviewPack:ReviewPack, * } */ function spec352SeedBlockedReviewOutput(ManagedEnvironment $environment, User $user, bool $withSuccessorDraft = false): array { $snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0, operationRunCount: 0); $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); $review->forceFill([ 'status' => EnvironmentReviewStatus::Published->value, 'published_at' => now()->subHour(), 'published_by_user_id' => (int) $user->getKey(), 'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [ 'publish_blockers' => ['Operator approval note is still missing.'], ]), ])->save(); $reviewPack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $environment->getKey(), 'workspace_id' => (int) $environment->workspace_id, 'environment_review_id' => (int) $review->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'file_path' => 'review-packs/spec352-review-output.zip', 'file_disk' => 'exports', 'generated_at' => now()->subMinutes(10), 'options' => [ 'include_pii' => false, 'include_operations' => true, ], ]); $review->forceFill([ 'current_export_review_pack_id' => (int) $reviewPack->getKey(), ])->save(); $successor = null; if ($withSuccessorDraft) { $successor = app(EnvironmentReviewLifecycleService::class)->createNextReview($review->fresh(), $user, $snapshot); } return [ 'review' => $review->fresh(), 'successor' => $successor?->fresh(), 'reviewPack' => $reviewPack->fresh(), ]; } it('prioritizes provider blockers over review output guidance when both are present', function (): void { [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); mockSpec352DashboardPermissions([ 'overall' => 'blocked', 'counts' => [ 'missing_application' => 1, 'missing_delegated' => 0, ], ]); spec352SeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true); $summary = app(EnvironmentDashboardSummaryBuilder::class) ->build($environment, $user) ->toArray(); expect(data_get($summary, 'operatorGuidance.key'))->toBe('provider_readiness.required_permissions') ->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_provider_blocked_title')) ->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Review permissions') ->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(RequiredPermissionsLinks::requiredPermissions($environment)) ->and(data_get($summary, 'readinessDecision.actionLabel'))->toBe('Review permissions') ->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull(); }); it('surfaces review output guidance with open draft review when provider blockers are absent', function (): void { [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); mockSpec352DashboardPermissions(); $reviewState = spec352SeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true); $successor = $reviewState['successor']; expect($successor)->not->toBeNull(); $summary = app(EnvironmentDashboardSummaryBuilder::class) ->build($environment, $user) ->toArray(); expect(data_get($summary, 'operatorGuidance.key'))->toBe('review_output.publication_blocked') ->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.review.draft_review_exists')) ->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Open draft review') ->and(data_get($summary, 'operatorGuidance.actionUrl'))->toContain('/environment-reviews/'.($successor?->getKey())) ->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull() ->and(collect(data_get($summary, 'operatorGuidance.secondaryActions', []))->pluck('actionLabel')->intersect([ 'Inspect review blockers', 'Open evidence basis', 'Open customer workspace', ])->isNotEmpty())->toBeTrue(); }); it('falls back to repo-backed operations attention when higher-priority guidance is absent', function (): void { [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); mockSpec352DashboardPermissions(); OperationRun::factory()->create([ 'managed_environment_id' => (int) $environment->getKey(), 'workspace_id' => (int) $environment->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'completed_at' => now()->subMinutes(5), ]); $summary = app(EnvironmentDashboardSummaryBuilder::class) ->build($environment, $user) ->toArray(); expect(data_get($summary, 'operatorGuidance.key'))->toBe('recommended_action.operations_requiring_attention') ->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_operations_title')) ->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Review operations') ->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(OperationRunLinks::index( $environment, activeTab: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, )); }); it('renders a calm no-urgent-action fallback when no dominant case exists', function (): void { [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); mockSpec352DashboardPermissions(); workspaceOverviewSeedQuietTenantTruth($environment); $backupSet = workspaceOverviewSeedHealthyBackup($environment); workspaceOverviewSeedRestoreHistory($environment, $backupSet, 'completed'); $summary = app(EnvironmentDashboardSummaryBuilder::class) ->build($environment, $user) ->toArray(); expect(data_get($summary, 'recommendedActions'))->toBe([]) ->and(data_get($summary, 'operatorGuidance.key'))->toBe('environment.no_urgent_action') ->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_no_urgent_title')) ->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe(__('localization.dashboard.overview.action_review_environment')) ->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(EnvironmentReviewResource::environmentScopedUrl('index', tenant: $environment)) ->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull(); });