create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: $role, workspaceRole: $workspaceRole); test()->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); return [$user, $tenant]; } function findingsHygienePage(?User $user = null, array $query = []) { if ($user instanceof User) { test()->actingAs($user); } setAdminPanelContext(); $factory = $query === [] ? Livewire::actingAs(auth()->user()) : Livewire::withQueryParams($query)->actingAs(auth()->user()); return $factory->test(FindingsHygieneReport::class); } function makeFindingsHygieneFinding(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 recordFindingsHygieneAudit(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('redirects hygiene report visits without workspace context into the existing workspace chooser flow', function (): void { $user = User::factory()->create(); $workspaceA = Workspace::factory()->create(); $workspaceB = Workspace::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspaceA->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspaceB->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); $this->actingAs($user) ->get(FindingsHygieneReport::getUrl(panel: 'admin')) ->assertRedirect('/admin/choose-workspace'); }); it('returns 404 for users outside the active workspace on the hygiene report route', function (): void { $user = User::factory()->create(); $workspace = Workspace::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) Workspace::factory()->create()->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get(FindingsHygieneReport::getUrl(panel: 'admin')) ->assertNotFound(); }); it('keeps the hygiene report accessible and calm for workspace members with zero visible hygiene scope', function (): void { $user = User::factory()->create(); $workspace = Workspace::factory()->create(['name' => 'Calm Workspace']); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'readonly', ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get(FindingsHygieneReport::getUrl(panel: 'admin')) ->assertOk() ->assertSee('Findings hygiene report') ->assertSee('No visible hygiene issues right now'); }); it('shows visible hygiene findings with reason labels, last activity, and row drilldown into tenant finding detail', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 22, 9, 0, 0, 'UTC')); [$user, $tenant] = findingsHygieneActingUser(); $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(); $brokenAssignment = makeFindingsHygieneFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'status' => Finding::STATUS_TRIAGED, 'subject_display_name' => 'Broken Assignment Finding', ]); $staleInProgress = makeFindingsHygieneFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(10), 'subject_display_name' => 'Stale Progress Finding', ]); recordFindingsHygieneAudit($staleInProgress, AuditActionId::FindingInProgress->value, CarbonImmutable::now()->subDays(10)); findingsHygienePage($user) ->assertCanSeeTableRecords([$brokenAssignment, $staleInProgress]) ->assertSee('Broken assignment') ->assertSee('Stale in progress') ->assertSee('Lost Member') ->assertSee('No current tenant membership'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(FindingsHygieneReport::getUrl(panel: 'admin')) ->assertOk() ->assertSee('Broken Assignment Finding') ->assertSee('/findings/'.$brokenAssignment->getKey(), false); }); it('suppresses hidden-tenant rows, counts, and tenant filter values inside an otherwise available report', function (): void { $visibleTenant = Tenant::factory()->create(['status' => 'active', 'name' => 'Visible Tenant']); [$user, $visibleTenant] = createUserWithTenant($visibleTenant, role: 'readonly', workspaceRole: 'readonly'); $hiddenTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $visibleTenant->workspace_id, 'name' => 'Hidden Tenant', ]); createUserWithTenant($hiddenTenant, $user, role: 'readonly', workspaceRole: 'readonly'); $visibleAssignee = User::factory()->create(['name' => 'Visible Assignee']); createUserWithTenant($visibleTenant, $visibleAssignee, role: 'readonly', workspaceRole: 'readonly'); TenantMembership::query() ->where('tenant_id', (int) $visibleTenant->getKey()) ->where('user_id', (int) $visibleAssignee->getKey()) ->delete(); $hiddenAssignee = User::factory()->create(['name' => 'Hidden Assignee']); createUserWithTenant($hiddenTenant, $hiddenAssignee, role: 'readonly', workspaceRole: 'readonly'); TenantMembership::query() ->where('tenant_id', (int) $hiddenTenant->getKey()) ->where('user_id', (int) $hiddenAssignee->getKey()) ->delete(); $visibleFinding = makeFindingsHygieneFinding($visibleTenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $visibleAssignee->getKey(), 'subject_display_name' => 'Visible Hygiene Finding', ]); $hiddenFinding = makeFindingsHygieneFinding($hiddenTenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $hiddenAssignee->getKey(), 'subject_display_name' => 'Hidden Hygiene Finding', ]); mock(CapabilityResolver::class, function ($mock) use ($visibleTenant, $hiddenTenant): void { $mock->shouldReceive('primeMemberships')->atLeast()->once(); $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(); }); }); $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $visibleTenant->workspace_id); $component = findingsHygienePage($user) ->assertCanSeeTableRecords([$visibleFinding]) ->assertCanNotSeeTableRecords([$hiddenFinding]) ->assertDontSee('Hidden Tenant'); expect($component->instance()->summaryCounts()) ->toBe([ 'unique_issue_count' => 1, 'broken_assignment_count' => 1, 'stale_in_progress_count' => 0, ]) ->and($component->instance()->availableFilters()) ->toBe([ [ 'key' => 'hygiene_scope', 'label' => 'Findings hygiene only', 'fixed' => true, 'options' => [], ], [ 'key' => 'tenant', 'label' => 'Tenant', 'fixed' => false, 'options' => [ ['value' => (string) $visibleTenant->getKey(), 'label' => $visibleTenant->name], ], ], ]); }); it('supports fixed reason filters without duplicating a multi-reason finding in the all-issues view', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 22, 9, 0, 0, 'UTC')); [$user, $tenant] = findingsHygieneActingUser(); $lostMember = User::factory()->create(['name' => 'Lost Worker']); createUserWithTenant($tenant, $lostMember, role: 'readonly', workspaceRole: 'readonly'); TenantMembership::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('user_id', (int) $lostMember->getKey()) ->delete(); $brokenOnly = makeFindingsHygieneFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'status' => Finding::STATUS_TRIAGED, 'subject_display_name' => 'Broken Only', ]); $staleOnly = makeFindingsHygieneFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(9), 'subject_display_name' => 'Stale Only', ]); recordFindingsHygieneAudit($staleOnly, AuditActionId::FindingInProgress->value, CarbonImmutable::now()->subDays(9)); $brokenAndStale = makeFindingsHygieneFinding($tenant, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(10), 'subject_display_name' => 'Broken And Stale', ]); recordFindingsHygieneAudit($brokenAndStale, AuditActionId::FindingInProgress->value, CarbonImmutable::now()->subDays(10)); $allIssues = findingsHygienePage($user); $brokenAssignments = findingsHygienePage($user, ['reason' => 'broken_assignment']); $staleInProgress = findingsHygienePage($user, ['reason' => 'stale_in_progress']); $allIssues ->assertCanSeeTableRecords([$brokenOnly, $staleOnly, $brokenAndStale]) ->assertSee('Broken And Stale'); $brokenAssignments ->assertCanSeeTableRecords([$brokenOnly, $brokenAndStale]) ->assertCanNotSeeTableRecords([$staleOnly]); $staleInProgress ->assertCanSeeTableRecords([$staleOnly, $brokenAndStale]) ->assertCanNotSeeTableRecords([$brokenOnly]); expect($allIssues->instance()->summaryCounts()) ->toBe([ 'unique_issue_count' => 3, 'broken_assignment_count' => 2, 'stale_in_progress_count' => 2, ]) ->and($allIssues->instance()->availableReasonFilters()) ->toBe([ [ 'key' => 'all', 'label' => 'All issues', 'active' => true, 'badge_count' => 3, 'url' => FindingsHygieneReport::getUrl(panel: 'admin'), ], [ 'key' => 'broken_assignment', 'label' => 'Broken assignment', 'active' => false, 'badge_count' => 2, 'url' => FindingsHygieneReport::getUrl(panel: 'admin', parameters: ['reason' => 'broken_assignment']), ], [ 'key' => 'stale_in_progress', 'label' => 'Stale in progress', 'active' => false, 'badge_count' => 2, 'url' => FindingsHygieneReport::getUrl(panel: 'admin', parameters: ['reason' => 'stale_in_progress']), ], ]); }); it('explains when the active tenant prefilter hides otherwise visible hygiene issues and clears it in place', function (): void { [$user, $tenantA] = findingsHygieneActingUser(); $tenantB = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantA->workspace_id, 'name' => 'Beta Tenant', ]); createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly'); $lostMember = User::factory()->create(['name' => 'Lost Member']); createUserWithTenant($tenantA, $lostMember, role: 'readonly', workspaceRole: 'readonly'); TenantMembership::query() ->where('tenant_id', (int) $tenantA->getKey()) ->where('user_id', (int) $lostMember->getKey()) ->delete(); $tenantAIssue = makeFindingsHygieneFinding($tenantA, [ 'owner_user_id' => (int) $user->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'subject_display_name' => 'Tenant A Issue', ]); session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); $component = findingsHygienePage($user) ->assertSet('tableFilters.tenant_id.value', (string) $tenantB->getKey()) ->assertCanNotSeeTableRecords([$tenantAIssue]) ->assertSee('No hygiene issues match this tenant scope') ->assertActionVisible('clear_tenant_filter'); $component->callAction('clear_tenant_filter') ->assertCanSeeTableRecords([$tenantAIssue]); expect($component->instance()->appliedScope()) ->toBe([ 'workspace_scoped' => true, 'fixed_scope' => 'visible_findings_hygiene_only', 'reason_filter' => 'all', 'reason_filter_label' => 'All issues', 'tenant_prefilter_source' => 'none', 'tenant_label' => null, ]); });