create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: $role, workspaceRole: $workspaceRole); return [$user, $tenant, $tenant->workspace()->firstOrFail()]; } function makeFindingsHygieneOverviewFinding(Tenant $tenant, array $attributes = []): Finding { $subjectDisplayName = $attributes['subject_display_name'] ?? null; unset($attributes['subject_display_name']); if (is_string($subjectDisplayName) && $subjectDisplayName !== '') { $attributes['evidence_jsonb'] = array_merge( is_array($attributes['evidence_jsonb'] ?? null) ? $attributes['evidence_jsonb'] : [], ['display_name' => $subjectDisplayName], ); } return Finding::factory()->for($tenant)->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, 'subject_external_id' => fake()->uuid(), 'status' => Finding::STATUS_TRIAGED, ], $attributes)); } function recordFindingsHygieneOverviewAudit(Finding $finding, string $action, CarbonImmutable $recordedAt): AuditLog { return AuditLog::query()->create([ 'workspace_id' => (int) $finding->workspace_id, 'tenant_id' => (int) $finding->tenant_id, 'action' => $action, 'status' => 'success', 'resource_type' => 'finding', 'resource_id' => (string) $finding->getKey(), 'summary' => 'Test workflow activity', 'recorded_at' => $recordedAt, ]); } it('adds a findings hygiene signal to the workspace overview and renders the report CTA', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 22, 9, 0, 0, 'UTC')); [$user, $tenant, $workspace] = findingsHygieneOverviewContext(); $lostMember = User::factory()->create(['name' => 'Lost Member']); createUserWithTenant($tenant, $lostMember, role: 'readonly', workspaceRole: 'readonly'); TenantMembership::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('user_id', (int) $lostMember->getKey()) ->delete(); makeFindingsHygieneOverviewFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'subject_display_name' => 'Broken Assignment', ]); $staleInProgress = makeFindingsHygieneOverviewFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(8), 'subject_display_name' => 'Stale In Progress', ]); recordFindingsHygieneOverviewAudit($staleInProgress, AuditActionId::FindingInProgress->value, CarbonImmutable::now()->subDays(8)); $signal = app(WorkspaceOverviewBuilder::class)->build($workspace, $user)['findings_hygiene_signal']; expect($signal) ->toBe([ 'headline' => '2 visible hygiene issues need follow-up', 'description' => '1 broken assignment and 1 stale in-progress finding need repair.', 'unique_issue_count' => 2, 'broken_assignment_count' => 1, 'stale_in_progress_count' => 1, 'is_calm' => false, 'cta_label' => 'Open hygiene report', 'cta_url' => FindingsHygieneReport::getUrl(panel: 'admin'), ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get('/admin') ->assertOk() ->assertSee('Findings hygiene') ->assertSee('Unique issues: 2') ->assertSee('Broken assignments: 1') ->assertSee('Stale in progress: 1') ->assertSee('Open hygiene report'); }); it('keeps the overview signal calm and suppresses hidden-tenant hygiene issues from counts and copy', function (): void { [$user, $visibleTenant, $workspace] = findingsHygieneOverviewContext(); $hiddenTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $visibleTenant->workspace_id, 'name' => 'Hidden Tenant', ]); createUserWithTenant($hiddenTenant, $user, role: 'readonly', workspaceRole: 'readonly'); $lostMember = User::factory()->create(['name' => 'Hidden Lost Member']); createUserWithTenant($hiddenTenant, $lostMember, role: 'readonly', workspaceRole: 'readonly'); TenantMembership::query() ->where('tenant_id', (int) $hiddenTenant->getKey()) ->where('user_id', (int) $lostMember->getKey()) ->delete(); makeFindingsHygieneOverviewFinding($hiddenTenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'subject_display_name' => 'Hidden Hygiene Issue', ]); mock(CapabilityResolver::class, function ($mock) use ($visibleTenant, $hiddenTenant): void { $mock->shouldReceive('primeMemberships')->atLeast()->once(); $mock->shouldReceive('isMember') ->andReturnUsing(static function (User $user, Tenant $tenant) use ($visibleTenant, $hiddenTenant): bool { expect([(int) $visibleTenant->getKey(), (int) $hiddenTenant->getKey()]) ->toContain((int) $tenant->getKey()); return true; }); $mock->shouldReceive('can') ->andReturnUsing(static function (User $user, Tenant $tenant, string $capability) use ($visibleTenant, $hiddenTenant): bool { expect([(int) $visibleTenant->getKey(), (int) $hiddenTenant->getKey()]) ->toContain((int) $tenant->getKey()); return $capability === Capabilities::TENANT_FINDINGS_VIEW && (int) $tenant->getKey() === (int) $visibleTenant->getKey(); }); }); $signal = app(WorkspaceOverviewBuilder::class)->build($workspace, $user)['findings_hygiene_signal']; expect($signal['unique_issue_count'])->toBe(0) ->and($signal['broken_assignment_count'])->toBe(0) ->and($signal['stale_in_progress_count'])->toBe(0) ->and($signal['is_calm'])->toBeTrue() ->and($signal['headline'])->toBe('Findings hygiene is calm') ->and($signal['description'])->toContain('No broken assignments or stale in-progress work are visible'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get('/admin') ->assertOk() ->assertSee('Findings hygiene is calm') ->assertSee('Unique issues: 0') ->assertSee('Calm') ->assertDontSee('Hidden Hygiene Issue'); });