create(['status' => 'active']); [$user, $tenantDashboard] = createUserWithTenant($tenantDashboard, role: 'owner', workspaceRole: 'readonly'); [$dashboardProfile, $dashboardSnapshot] = seedActiveBaselineForTenant($tenantDashboard); seedBaselineCompareRun($tenantDashboard, $dashboardProfile, $dashboardSnapshot, workspaceOverviewCompareCoverage()); 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()); 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), ); $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()); 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()); 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_follow_up')['destination']['kind'])->toBe('operations_index') ->and($items->get('tenant_operations_follow_up')['destination']['url'])->toContain('activeTab=blocked') ->and($items->get('tenant_operations_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); });