create(['status' => 'active']); [$user, $tenantA] = createUserWithTenant($tenantA, role: 'readonly', workspaceRole: 'readonly'); $tenantB = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantA->workspace_id, 'name' => 'Bravo Tenant', ]); createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly'); $hiddenTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $tenantA->workspace_id, 'name' => 'Hidden Tenant', ]); Finding::factory()->for($tenantA)->create([ 'workspace_id' => (int) $tenantA->workspace_id, 'assignee_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_NEW, 'due_at' => now()->subHour(), ]); Finding::factory()->for($tenantB)->create([ 'workspace_id' => (int) $tenantB->workspace_id, 'assignee_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_TRIAGED, 'due_at' => now()->addDay(), ]); Finding::factory()->for($tenantB)->create([ 'workspace_id' => (int) $tenantB->workspace_id, 'assignee_user_id' => null, 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_NEW, ]); Finding::factory()->for($hiddenTenant)->create([ 'workspace_id' => (int) $hiddenTenant->workspace_id, 'assignee_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_NEW, ]); $signal = app(WorkspaceOverviewBuilder::class) ->build($tenantA->workspace()->firstOrFail(), $user)['my_findings_signal']; expect($signal['open_assigned_count'])->toBe(2) ->and($signal['overdue_assigned_count'])->toBe(1) ->and($signal['is_calm'])->toBeFalse() ->and($signal['cta_label'])->toBe('Open my findings') ->and($signal['cta_url'])->toBe(MyFindingsInbox::getUrl(panel: 'admin')); }); it('keeps the signal calm when no visible assigned findings remain', function (): void { $tenant = Tenant::factory()->create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: 'readonly', workspaceRole: 'readonly'); Finding::factory()->for($tenant)->create([ 'workspace_id' => (int) $tenant->workspace_id, 'assignee_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_RESOLVED, 'resolved_at' => now(), ]); $signal = app(WorkspaceOverviewBuilder::class) ->build($tenant->workspace()->firstOrFail(), $user)['my_findings_signal']; expect($signal['open_assigned_count'])->toBe(0) ->and($signal['overdue_assigned_count'])->toBe(0) ->and($signal['is_calm'])->toBeTrue() ->and($signal['description'])->toContain('visible assigned'); }); it('suppresses blocked-tenant findings from the assigned-to-me signal', function (): void { $visibleTenant = Tenant::factory()->create(['status' => 'active']); [$user, $visibleTenant] = createUserWithTenant($visibleTenant, role: 'readonly', workspaceRole: 'readonly'); $blockedTenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $visibleTenant->workspace_id, 'name' => 'Blocked Tenant', ]); createUserWithTenant($blockedTenant, $user, role: 'readonly', workspaceRole: 'readonly'); Finding::factory()->for($visibleTenant)->create([ 'workspace_id' => (int) $visibleTenant->workspace_id, 'assignee_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_NEW, ]); Finding::factory()->for($blockedTenant)->create([ 'workspace_id' => (int) $blockedTenant->workspace_id, 'assignee_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => Finding::STATUS_NEW, ]); mock(CapabilityResolver::class, function ($mock) use ($visibleTenant, $blockedTenant): void { $mock->shouldReceive('primeMemberships')->once(); $mock->shouldReceive('isMember')->andReturnTrue(); $mock->shouldReceive('can') ->andReturnUsing(static function (User $user, Tenant $tenant, string $capability) use ($visibleTenant, $blockedTenant): bool { expect([(int) $visibleTenant->getKey(), (int) $blockedTenant->getKey()]) ->toContain((int) $tenant->getKey()); return match ($capability) { Capabilities::TENANT_FINDINGS_VIEW => (int) $tenant->getKey() === (int) $visibleTenant->getKey(), default => false, }; }); }); $signal = app(WorkspaceOverviewBuilder::class) ->build($visibleTenant->workspace()->firstOrFail(), $user)['my_findings_signal']; expect($signal['open_assigned_count'])->toBe(1) ->and($signal['overdue_assigned_count'])->toBe(0) ->and($signal['description'])->not->toContain($blockedTenant->name); });