create(['status' => 'active']); [$user, $tenantDashboard] = createUserWithTenant($tenantDashboard, role: 'owner', workspaceRole: 'readonly'); [$dashboardProfile, $dashboardSnapshot] = seedActiveBaselineForTenant($tenantDashboard); seedBaselineCompareRun($tenantDashboard, $dashboardProfile, $dashboardSnapshot, workspaceOverviewCompareCoverage()); $tenantDashboardBackup = workspaceOverviewSeedHealthyBackup($tenantDashboard, [ 'completed_at' => now()->subMinutes(15), ]); workspaceOverviewSeedRestoreHistory($tenantDashboard, $tenantDashboardBackup, 'completed'); Finding::factory()->riskAccepted()->create([ 'workspace_id' => (int) $tenantDashboard->workspace_id, 'tenant_id' => (int) $tenantDashboard->getKey(), ]); $tenantFindings = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantDashboard->workspace_id, ]); createUserWithTenant($tenantFindings, $user, role: 'owner', workspaceRole: 'readonly'); [$findingsProfile, $findingsSnapshot] = seedActiveBaselineForTenant($tenantFindings); seedBaselineCompareRun($tenantFindings, $findingsProfile, $findingsSnapshot, workspaceOverviewCompareCoverage()); $tenantFindingsBackup = workspaceOverviewSeedHealthyBackup($tenantFindings, [ 'completed_at' => now()->subMinutes(14), ]); workspaceOverviewSeedRestoreHistory($tenantFindings, $tenantFindingsBackup, 'completed'); Finding::factory()->for($tenantFindings)->create([ 'workspace_id' => (int) $tenantFindings->workspace_id, 'status' => Finding::STATUS_TRIAGED, 'due_at' => now()->subDay(), ]); $tenantCompare = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantDashboard->workspace_id, 'name' => 'Compare Tenant', ]); createUserWithTenant($tenantCompare, $user, role: 'owner', workspaceRole: 'readonly'); [$compareProfile, $compareSnapshot] = seedActiveBaselineForTenant($tenantCompare); seedBaselineCompareRun( $tenantCompare, $compareProfile, $compareSnapshot, workspaceOverviewCompareCoverage(), completedAt: now()->subDays(10), ); $tenantCompareBackup = workspaceOverviewSeedHealthyBackup($tenantCompare, [ 'completed_at' => now()->subMinutes(13), ]); workspaceOverviewSeedRestoreHistory($tenantCompare, $tenantCompareBackup, 'completed'); $tenantOperations = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantDashboard->workspace_id, 'name' => 'Operations Tenant', ]); createUserWithTenant($tenantOperations, $user, role: 'owner', workspaceRole: 'readonly'); [$operationsProfile, $operationsSnapshot] = seedActiveBaselineForTenant($tenantOperations); seedBaselineCompareRun($tenantOperations, $operationsProfile, $operationsSnapshot, workspaceOverviewCompareCoverage()); $tenantOperationsBackup = workspaceOverviewSeedHealthyBackup($tenantOperations, [ 'completed_at' => now()->subMinutes(12), ]); workspaceOverviewSeedRestoreHistory($tenantOperations, $tenantOperationsBackup, 'completed'); OperationRun::factory()->create([ 'tenant_id' => (int) $tenantOperations->getKey(), 'workspace_id' => (int) $tenantOperations->workspace_id, 'type' => OperationRunType::PolicySync->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); $tenantAlerts = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantDashboard->workspace_id, 'name' => 'Alerts Tenant', ]); createUserWithTenant($tenantAlerts, $user, role: 'owner', workspaceRole: 'readonly'); [$alertsProfile, $alertsSnapshot] = seedActiveBaselineForTenant($tenantAlerts); seedBaselineCompareRun($tenantAlerts, $alertsProfile, $alertsSnapshot, workspaceOverviewCompareCoverage()); $tenantAlertsBackup = workspaceOverviewSeedHealthyBackup($tenantAlerts, [ 'completed_at' => now()->subMinutes(11), ]); workspaceOverviewSeedRestoreHistory($tenantAlerts, $tenantAlertsBackup, 'completed'); AlertDelivery::factory()->create([ 'tenant_id' => (int) $tenantAlerts->getKey(), 'workspace_id' => (int) $tenantAlerts->workspace_id, 'status' => AlertDelivery::STATUS_FAILED, 'created_at' => now(), ]); $workspace = $tenantDashboard->workspace()->firstOrFail(); $overview = app(WorkspaceOverviewBuilder::class)->build($workspace, $user); $items = collect($overview['attention_items'])->keyBy('key'); expect($items->get('tenant_lapsed_governance')['destination']['kind'])->toBe('tenant_dashboard') ->and($items->get('tenant_overdue_findings')['destination']['kind'])->toBe('tenant_findings') ->and($items->get('tenant_overdue_findings')['destination']['url'])->toContain('tab=overdue') ->and($items->get('tenant_compare_attention')['destination']['kind'])->toBe('baseline_compare_landing') ->and($items->get('tenant_operations_terminal_follow_up')['destination']['kind'])->toBe('operations_index') ->and($items->get('tenant_operations_terminal_follow_up')['destination']['url'])->toContain('activeTab=terminal_follow_up') ->and($items->get('tenant_operations_terminal_follow_up')['destination']['url'])->toContain('problemClass=terminal_follow_up') ->and($items->get('tenant_operations_terminal_follow_up')['destination']['url'])->toContain('tenant_id='.(string) $tenantOperations->getKey()) ->and($items->get('tenant_alert_delivery_failures')['destination']['kind'])->toBe('alerts_overview') ->and($items->get('tenant_alert_delivery_failures')['destination']['url'])->toContain('nav%5Bback_url%5D='); }); it('renders evidence and review actions through the shared attention-item contract', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'readonly'); $this->actingAs($user); $evidenceUrl = EvidenceSnapshotResource::getUrl('index', panel: 'tenant', tenant: $tenant); $reviewUrl = TenantReviewResource::tenantScopedUrl('index', [], $tenant); $items = [ [ 'key' => 'tenant_evidence_attention', 'tenant_id' => (int) $tenant->getKey(), 'tenant_label' => (string) $tenant->name, 'tenant_route_key' => (string) $tenant->external_id, 'family' => 'evidence', 'urgency' => 'high', 'title' => 'Evidence needs refresh', 'body' => 'Current evidence should be refreshed before relying on it.', 'supporting_message' => null, 'badge' => 'Evidence', 'badge_color' => 'warning', 'destination' => [ 'kind' => 'tenant_evidence', 'url' => $evidenceUrl, 'tenant_route_key' => (string) $tenant->external_id, 'label' => 'Open evidence', 'disabled' => false, 'helper_text' => null, 'filters' => null, ], 'action_disabled' => false, 'helper_text' => null, 'url' => $evidenceUrl, ], [ 'key' => 'tenant_review_attention', 'tenant_id' => (int) $tenant->getKey(), 'tenant_label' => (string) $tenant->name, 'tenant_route_key' => (string) $tenant->external_id, 'family' => 'review', 'urgency' => 'medium', 'title' => 'Review needs publication work', 'body' => 'The current review is still internal-only.', 'supporting_message' => null, 'badge' => 'Review', 'badge_color' => 'warning', 'destination' => [ 'kind' => 'tenant_reviews', 'url' => $reviewUrl, 'tenant_route_key' => (string) $tenant->external_id, 'label' => 'Open review', 'disabled' => false, 'helper_text' => null, 'filters' => null, ], 'action_disabled' => false, 'helper_text' => null, 'url' => $reviewUrl, ], ]; $component = Livewire::test(WorkspaceNeedsAttention::class, [ 'items' => $items, 'emptyState' => [], ]) ->assertSee('Evidence needs refresh') ->assertSee('Open evidence') ->assertSee('Review needs publication work') ->assertSee('Open review'); expect($component->html()) ->toContain($evidenceUrl) ->toContain($reviewUrl); }); it('hydrates filtered tenant-registry triage state from multi-tenant workspace backup and recovery metrics', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 9, 9, 0, 0, 'UTC')); $absentTenant = Tenant::factory()->create([ 'status' => 'active', 'name' => 'Absent Backup Tenant', ]); [$user, $absentTenant] = createUserWithTenant($absentTenant, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($absentTenant); $staleTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $absentTenant->workspace_id, 'name' => 'Stale Backup Tenant', ]); createUserWithTenant($staleTenant, $user, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($staleTenant); $staleBackup = workspaceOverviewSeedHealthyBackup($staleTenant, [ 'completed_at' => now()->subDays(2), ]); workspaceOverviewSeedRestoreHistory($staleTenant, $staleBackup, 'completed'); $degradedTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $absentTenant->workspace_id, 'name' => 'Degraded Backup Tenant', ]); createUserWithTenant($degradedTenant, $user, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($degradedTenant); $degradedBackup = workspaceOverviewSeedHealthyBackup($degradedTenant, [ 'completed_at' => now()->subMinutes(20), ], [ 'payload' => [], 'metadata' => [ 'source' => 'metadata_only', 'assignments_fetch_failed' => true, ], 'assignments' => [], ]); workspaceOverviewSeedRestoreHistory($degradedTenant, $degradedBackup, 'completed'); $weakenedTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $absentTenant->workspace_id, 'name' => 'Weakened Recovery Tenant', ]); createUserWithTenant($weakenedTenant, $user, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($weakenedTenant); $weakenedBackup = workspaceOverviewSeedHealthyBackup($weakenedTenant, [ 'completed_at' => now()->subMinutes(18), ]); workspaceOverviewSeedRestoreHistory($weakenedTenant, $weakenedBackup, 'follow_up'); $unvalidatedTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $absentTenant->workspace_id, 'name' => 'Unvalidated Recovery Tenant', ]); createUserWithTenant($unvalidatedTenant, $user, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($unvalidatedTenant); workspaceOverviewSeedHealthyBackup($unvalidatedTenant, [ 'completed_at' => now()->subMinutes(16), ]); $calmTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $absentTenant->workspace_id, 'name' => 'Calm Tenant', ]); createUserWithTenant($calmTenant, $user, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($calmTenant); $calmBackup = workspaceOverviewSeedHealthyBackup($calmTenant, [ 'completed_at' => now()->subMinutes(14), ]); workspaceOverviewSeedRestoreHistory($calmTenant, $calmBackup, 'completed'); $workspace = $absentTenant->workspace()->firstOrFail(); $metrics = collect(app(WorkspaceOverviewBuilder::class)->build($workspace, $user)['summary_metrics'])->keyBy('key'); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); Filament::setTenant(null, true); parse_str((string) parse_url((string) $metrics->get('backup_attention_tenants')['destination_url'], PHP_URL_QUERY), $backupQuery); $backupRegistry = Livewire::withQueryParams($backupQuery) ->actingAs($user) ->test(ListTenants::class) ->assertSet('tableFilters.backup_posture.values', [ 'absent', 'stale', 'degraded', ]) ->assertSet('tableFilters.triage_sort.value', TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); expect($backupRegistry->instance()->getFilteredSortedTableQuery()?->pluck('tenants.name')->all()) ->toBe([ 'Absent Backup Tenant', 'Stale Backup Tenant', 'Degraded Backup Tenant', ]); parse_str((string) parse_url((string) $metrics->get('recovery_attention_tenants')['destination_url'], PHP_URL_QUERY), $recoveryQuery); $recoveryRegistry = Livewire::withQueryParams($recoveryQuery) ->actingAs($user) ->test(ListTenants::class) ->assertSet('tableFilters.recovery_evidence.values', [ 'weakened', 'unvalidated', ]) ->assertSet('tableFilters.triage_sort.value', TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); expect($recoveryRegistry->instance()->getFilteredSortedTableQuery()?->pluck('tenants.name')->all()) ->toBe([ 'Absent Backup Tenant', 'Weakened Recovery Tenant', 'Unvalidated Recovery Tenant', ]); }); it('routes backup and recovery workspace attention into tenant dashboards that still show the same weakness', function (): void { $backupTenant = Tenant::factory()->create([ 'status' => 'active', 'name' => 'Backup Weak Tenant', ]); [$user, $backupTenant] = createUserWithTenant($backupTenant, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($backupTenant); $recoveryTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $backupTenant->workspace_id, 'name' => 'Recovery Weak Tenant', ]); createUserWithTenant($recoveryTenant, $user, role: 'owner', workspaceRole: 'readonly'); workspaceOverviewSeedQuietTenantTruth($recoveryTenant); $recoveryBackup = workspaceOverviewSeedHealthyBackup($recoveryTenant, [ 'completed_at' => now()->subMinutes(20), ]); workspaceOverviewSeedRestoreHistory($recoveryTenant, $recoveryBackup, 'follow_up'); $workspace = $backupTenant->workspace()->firstOrFail(); $overview = app(WorkspaceOverviewBuilder::class)->build($workspace, $user); $items = collect($overview['attention_items'])->keyBy('key'); $backupDestination = $items->get('tenant_backup_absent')['destination']; $recoveryDestination = $items->get('tenant_recovery_weakened')['destination']; expect($backupDestination['kind'])->toBe('tenant_dashboard') ->and($recoveryDestination['kind'])->toBe('tenant_dashboard'); $this->actingAs($user); Filament::setCurrentPanel(Filament::getPanel('tenant')); Filament::setTenant($backupTenant, true); Livewire::test(TenantNeedsAttention::class) ->assertSee('No usable backup basis'); Filament::setCurrentPanel(Filament::getPanel('tenant')); Filament::setTenant($recoveryTenant, true); Livewire::test(TenantNeedsAttention::class) ->assertSee('Recent restore needs follow-up'); });