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), ]); }); } it('renders the decision-first tenant overview with the capped first-screen structure', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); mockTenantDashboardSummaryPermissions(); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun($tenant, $profile, $snapshot, workspaceOverviewCompareCoverage()); $backupSet = workspaceOverviewSeedHealthyBackup($tenant); workspaceOverviewSeedRestoreHistory($tenant, $backupSet); Finding::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'severity' => Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_NEW, ]); OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'completed_at' => now()->subHour(), ]); ProviderConnection::factory()->platform()->consentGranted()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'is_default' => true, ]); $this->actingAs($user); setTenantPanelContext($tenant); $response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) ->assertSuccessful() ->assertSee($tenant->name) ->assertSee('Recommended next actions') ->assertSee('Governance status') ->assertSee('Operations needing attention') ->assertSee('Current review') ->assertSee('Risk exceptions') ->assertSee('Provider Health') ->assertSee('Customer-safe output') ->assertSee('Operations requiring attention') ->assertSee('Review operation') ->assertSee('Open operations hub') ->assertDontSee('Recent operations'); $content = $response->getContent(); $contextChipsPosition = strpos($content, 'data-testid="tenant-dashboard-context-chips"'); $firstKpiPosition = strpos($content, 'data-testid="tenant-dashboard-kpi"'); $governanceStatusCount = substr_count($content, 'data-testid="tenant-dashboard-governance-status"'); $secondaryListRowCount = substr_count($content, 'data-overview-row-style="secondary-list-row"'); expect(substr_count($content, 'data-testid="tenant-dashboard-kpi"'))->toBe(4) ->and($content)->toContain('data-testid="tenant-dashboard-posture-pill"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chips"') ->and($content)->toContain('class="flex w-full flex-col items-start gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-start md:flex-nowrap"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-workspace"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-workspace" class="inline-flex min-w-0 w-full max-w-full items-center') ->and($content)->toContain('sm:w-auto sm:max-w-[20rem] lg:max-w-[24rem]') ->and($content)->toContain('Workspace: '.$tenant->workspace->name) ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider"') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider-microsoft-logo"') ->and($content)->toContain('data-provider-key="microsoft"') ->and($content)->toContain('Microsoft tenant') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity" class="inline-flex items-center gap-2 whitespace-nowrap') ->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity-icon"') ->and($content)->toContain('Latest activity:') ->and($contextChipsPosition)->not->toBeFalse() ->and($firstKpiPosition)->not->toBeFalse() ->and($contextChipsPosition)->toBeLessThan($firstKpiPosition) ->and($secondaryListRowCount)->toBe($governanceStatusCount) ->and($content)->toContain('hover:shadow-md') ->and($content)->toContain('hover:ring-1') ->and(substr_count($content, 'data-kpi-has-icon="true"'))->toBe(4) ->and(substr_count($content, 'data-kpi-has-chart="true"'))->toBe(2) ->and(substr_count($content, 'data-testid="tenant-dashboard-recommended-action"'))->toBeLessThanOrEqual(3) ->and(substr_count($content, 'tenant-dashboard-recommended-actions'))->toBeGreaterThanOrEqual(1) ->and(substr_count($content, 'data-testid="tenant-dashboard-governance-status-icon"'))->toBe(substr_count($content, 'data-testid="tenant-dashboard-governance-status"')) ->and(substr_count($content, 'data-testid="tenant-dashboard-operations-attention-item-icon"'))->toBeGreaterThanOrEqual(1) ->and(substr_count($content, 'data-testid="tenant-dashboard-readiness-card"'))->toBe(4) ->and($content)->toContain('data-readiness-key="provider_health"') ->and($content)->not->toContain('Open customer workspace') ->and($content)->not->toContain('fixed bottom-4 right-4 z-[999999] w-96 space-y-2') ->and($content)->toContain('High severity findings') ->and($content)->not->toContain('section_recent_operations'); }); it('adds repo-real icon metadata and only supported sparkline series to tenant dashboard kpis', function (): void { Carbon::setTestNow('2026-05-03 12:00:00'); try { [$user, $tenant] = createUserWithTenant(role: 'owner'); mockTenantDashboardSummaryPermissions([ 'counts' => [ 'missing_application' => 2, 'missing_delegated' => 1, ], ]); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun($tenant, $profile, $snapshot, workspaceOverviewCompareCoverage()); $backupSet = workspaceOverviewSeedHealthyBackup($tenant); workspaceOverviewSeedRestoreHistory($tenant, $backupSet); foreach ([6, 6, 4, 1] as $daysAgo) { Finding::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'severity' => $daysAgo === 4 ? Finding::SEVERITY_CRITICAL : Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_NEW, 'first_seen_at' => now()->subDays($daysAgo), 'last_seen_at' => now()->subDays($daysAgo), ]); } Finding::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'severity' => Finding::SEVERITY_MEDIUM, 'status' => Finding::STATUS_NEW, 'first_seen_at' => now()->subDays(2), 'last_seen_at' => now()->subDays(2), ]); Finding::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'severity' => Finding::SEVERITY_MEDIUM, 'status' => Finding::STATUS_NEW, 'first_seen_at' => now()->subDays(2), 'last_seen_at' => now()->subDays(2), 'due_at' => now()->subDay(), ]); foreach ([5, 2, 2] as $daysAgo) { OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'created_at' => now()->subDays($daysAgo)->subHours(3), 'completed_at' => now()->subDays($daysAgo), ]); } $kpis = collect(app(TenantDashboardSummaryBuilder::class) ->build($tenant, $user) ->toArray()['kpis']) ->keyBy('key'); expect($kpis->keys()->all())->toBe([ 'high_severity_findings', 'overdue_findings', 'missing_permissions', 'active_operations', ]) ->and($kpis['active_operations']['label'])->toBe('Operations needing attention') ->and($kpis->pluck('icon')->filter()->count())->toBe(4) ->and($kpis['high_severity_findings']['icon'])->toBe('heroicon-m-arrow-trending-up') ->and($kpis['high_severity_findings']['description'])->toBe('4 active · 4 new in 7d') ->and($kpis['high_severity_findings']['chart'])->toBe([2, 0, 1, 0, 0, 1, 0]) ->and($kpis['overdue_findings']['icon'])->toBe('heroicon-m-arrow-trending-up') ->and($kpis['overdue_findings']['description'])->toBe('1 overdue now') ->and($kpis['missing_permissions']['icon'])->toBe('heroicon-m-arrow-trending-up') ->and($kpis['missing_permissions']['description'])->toBe('2 app · 1 delegated missing') ->and($kpis['active_operations']['icon'])->toBe('heroicon-m-arrow-trending-up') ->and($kpis['active_operations']['description'])->toBe('3 operations require attention') ->and($kpis['active_operations']['chart'])->toBe([0, 1, 0, 0, 2, 0, 0]) ->and($kpis['overdue_findings']['chart'])->toBeNull() ->and($kpis['missing_permissions']['chart'])->toBeNull(); } finally { Carbon::setTestNow(); } }); it('adds semantic icon metadata to governance status rows and curated operations attention items', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); mockTenantDashboardSummaryPermissions(); OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'created_at' => now()->subMinutes(3), 'completed_at' => now()->subMinutes(3), ]); OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'tenant.review_pack.generate', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'created_at' => now()->subMinutes(2), 'completed_at' => now()->subMinutes(2), ]); OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => OperationCatalog::TYPE_PERMISSION_POSTURE_CHECK, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Blocked->value, 'created_at' => now()->subMinute(), 'completed_at' => now()->subMinute(), ]); $summary = app(TenantDashboardSummaryBuilder::class) ->build($tenant, $user) ->toArray(); $governanceStatus = collect($summary['governanceStatus'])->keyBy('key'); $attentionOperations = collect($summary['activeOperationSummary']['items'] ?? [])->keyBy('type'); expect($governanceStatus['baseline_compare']['icon'] ?? null)->toBe('heroicon-m-arrows-right-left') ->and($governanceStatus['evidence_coverage']['icon'] ?? null)->toBe('heroicon-m-document-check') ->and($governanceStatus['review_freshness']['icon'] ?? null)->toBe('heroicon-m-clipboard-document-check') ->and($governanceStatus['provider_permissions']['icon'] ?? null)->toBe('heroicon-m-key') ->and($governanceStatus['backup_posture']['icon'] ?? null)->toBe('heroicon-m-archive-box') ->and($attentionOperations['Inventory sync']['icon'] ?? null)->toBe('heroicon-m-arrow-path') ->and($attentionOperations['Review pack generation']['icon'] ?? null)->toBe('heroicon-m-document-arrow-down') ->and($attentionOperations['Permission posture check']['icon'] ?? null)->toBe('heroicon-m-key'); }); it('shows calm honest fallbacks when no urgent tenant follow-up is visible', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); mockTenantDashboardSummaryPermissions(); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun($tenant, $profile, $snapshot, workspaceOverviewCompareCoverage()); $backupSet = workspaceOverviewSeedHealthyBackup($tenant); workspaceOverviewSeedRestoreHistory($tenant, $backupSet); $this->actingAs($user); setTenantPanelContext($tenant); $response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) ->assertSuccessful() ->assertSee('No immediate action is waiting.') ->assertDontSee('Recent operations') ->assertDontSee('Operations requiring attention'); $content = $response->getContent(); expect(substr_count($content, 'data-testid="tenant-dashboard-recommended-actions-empty"'))->toBe(1) ->and($content)->not->toContain('data-testid="tenant-dashboard-operations-attention-summary"') ->and($content)->not->toContain('data-testid="tenant-dashboard-recent-operations-empty"'); }); it('builds a curated operations requiring attention summary and excludes healthy active runs', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); mockTenantDashboardSummaryPermissions(); $healthyRunningRun = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Running->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinute(), 'started_at' => now()->subMinute(), ]); $followUpRun = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'created_at' => now()->subMinutes(6), 'started_at' => now()->subMinutes(5), 'completed_at' => now()->subMinutes(4), ]); $blockedRun = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => OperationCatalog::TYPE_PERMISSION_POSTURE_CHECK, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Blocked->value, 'created_at' => now()->subMinutes(5), 'started_at' => now()->subMinutes(4), 'completed_at' => now()->subMinutes(3), ]); $summary = app(TenantDashboardSummaryBuilder::class) ->build($tenant, $user) ->toArray(); $activeOperationSummary = $summary['activeOperationSummary'] ?? null; $items = collect($activeOperationSummary['items'] ?? []); expect($activeOperationSummary) ->not->toBeNull() ->and($activeOperationSummary['title'] ?? null)->toBe('Operations requiring attention') ->and($activeOperationSummary['count'] ?? null)->toBe(2) ->and($activeOperationSummary['secondaryActionLabel'] ?? null)->toBe('Open operations hub') ->and($activeOperationSummary['secondaryActionUrl'] ?? null)->toBe(OperationRunLinks::index( $tenant, activeTab: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, )) ->and($items)->toHaveCount(2) ->and($items->pluck('id')->all())->toBe([ (int) $blockedRun->getKey(), (int) $followUpRun->getKey(), ]) ->and($items->pluck('primaryActionLabel')->unique()->all())->toBe(['Review operation']) ->and($items->pluck('primaryActionUrl')->all())->toBe([ OperationRunLinks::view($blockedRun, $tenant), OperationRunLinks::view($followUpRun, $tenant), ]) ->and($items->pluck('attentionLabel')->unique()->all())->toBe(['Follow-up required']) ->and($items->pluck('timingLabel')->filter()->isNotEmpty())->toBeTrue() ->and($items->pluck('outcomeSentence')->filter()->isNotEmpty())->toBeTrue() ->and($items->pluck('reason')->filter()->isNotEmpty())->toBeTrue() ->and($items->pluck('impact')->filter()->isNotEmpty())->toBeTrue() ->and($items->pluck('id')->contains((int) $healthyRunningRun->getKey()))->toBeFalse(); $this->actingAs($user); setTenantPanelContext($tenant); $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) ->assertSuccessful() ->assertSee('data-testid="tenant-dashboard-operations-attention-summary"', false) ->assertSee('Review operation') ->assertSee('Open operations hub') ->assertSee('Completed '.$followUpRun->completed_at?->diffForHumans()) ->assertSee('Inventory sync') ->assertDontSee('Operation #'.$followUpRun->getKey()) ->assertDontSee('Recent operations'); }); it('omits the compact active operations summary when no qualifying visible run exists', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); mockTenantDashboardSummaryPermissions(); OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'created_at' => now()->subMinutes(3), 'started_at' => now()->subMinutes(2), 'completed_at' => now()->subMinute(), ]); $summary = app(TenantDashboardSummaryBuilder::class) ->build($tenant, $user) ->toArray(); expect($summary['activeOperationSummary'] ?? null)->toBeNull(); $this->actingAs($user); setTenantPanelContext($tenant); $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant)) ->assertSuccessful() ->assertDontSee('data-testid="tenant-dashboard-operations-attention-summary"', false) ->assertDontSee('Review operation') ->assertDontSee('Open operations hub') ->assertDontSee('Recent operations'); });