create(['status' => 'active']); [$user, $tenant] = createUserWithTenant($tenant, role: $role, workspaceRole: $workspaceRole); return [ app(FindingAssignmentHygieneService::class), $user, $tenant->workspace()->firstOrFail(), $tenant, ]; } function assignmentHygieneFinding(Tenant $tenant, array $attributes = []): Finding { return Finding::factory()->for($tenant)->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, 'status' => Finding::STATUS_TRIAGED, 'subject_external_id' => fake()->uuid(), ], $attributes)); } function recordAssignmentHygieneWorkflowAudit(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('classifies broken assignments from current tenant entitlement loss and soft-deleted assignees', function (): void { [$service, $viewer, $workspace, $tenant] = assignmentHygieneServiceContext(); $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(); $softDeletedAssignee = User::factory()->create(['name' => 'Deleted Member']); createUserWithTenant($tenant, $softDeletedAssignee, role: 'readonly', workspaceRole: 'readonly'); $softDeletedAssignee->delete(); $healthyAssignee = User::factory()->create(['name' => 'Healthy Assignee']); createUserWithTenant($tenant, $healthyAssignee, role: 'readonly', workspaceRole: 'readonly'); $brokenByMembership = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'status' => Finding::STATUS_TRIAGED, 'subject_external_id' => 'broken-membership', ]); $brokenBySoftDelete = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $softDeletedAssignee->getKey(), 'status' => Finding::STATUS_NEW, 'subject_external_id' => 'broken-soft-delete', ]); $healthyAssigned = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $healthyAssignee->getKey(), 'status' => Finding::STATUS_NEW, 'subject_external_id' => 'healthy-assigned', ]); $ordinaryIntake = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => null, 'status' => Finding::STATUS_NEW, 'subject_external_id' => 'ordinary-intake', ]); $issues = $service->issueQuery($workspace, $viewer)->get()->keyBy('id'); $summary = $service->summary($workspace, $viewer); expect($issues->keys()->all()) ->toContain((int) $brokenByMembership->getKey(), (int) $brokenBySoftDelete->getKey()) ->not->toContain((int) $healthyAssigned->getKey(), (int) $ordinaryIntake->getKey()) ->and($service->reasonLabelsFor($issues[$brokenByMembership->getKey()])) ->toBe(['Broken assignment']) ->and($service->reasonLabelsFor($issues[$brokenBySoftDelete->getKey()])) ->toBe(['Broken assignment']) ->and($issues[$brokenBySoftDelete->getKey()]->assigneeUser?->name) ->toBe('Deleted Member') ->and($summary) ->toBe([ 'unique_issue_count' => 2, 'broken_assignment_count' => 2, 'stale_in_progress_count' => 0, ]); }); it('classifies stale in-progress work from meaningful workflow activity and excludes recently advanced or merely overdue work', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 22, 9, 0, 0, 'UTC')); [$service, $viewer, $workspace, $tenant] = assignmentHygieneServiceContext(); $staleFinding = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $viewer->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(10), 'subject_external_id' => 'stale-finding', ]); recordAssignmentHygieneWorkflowAudit($staleFinding, AuditActionId::FindingInProgress->value, CarbonImmutable::now()->subDays(10)); $recentlyAssigned = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $viewer->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(10), 'subject_external_id' => 'recently-assigned', ]); recordAssignmentHygieneWorkflowAudit($recentlyAssigned, AuditActionId::FindingAssigned->value, CarbonImmutable::now()->subDays(2)); $recentlyReopened = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $viewer->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(10), 'reopened_at' => now()->subDay(), 'subject_external_id' => 'recently-reopened', ]); recordAssignmentHygieneWorkflowAudit($recentlyReopened, AuditActionId::FindingReopened->value, CarbonImmutable::now()->subDay()); $overdueButActive = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $viewer->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(10), 'due_at' => now()->subDay(), 'subject_external_id' => 'overdue-but-active', ]); recordAssignmentHygieneWorkflowAudit($overdueButActive, AuditActionId::FindingAssigned->value, CarbonImmutable::now()->subHours(12)); $issues = $service->issueQuery( $workspace, $viewer, reasonFilter: FindingAssignmentHygieneService::REASON_STALE_IN_PROGRESS, )->get(); $summary = $service->summary($workspace, $viewer); expect($issues->pluck('id')->all()) ->toBe([(int) $staleFinding->getKey()]) ->and($service->reasonLabelsFor($issues->firstOrFail())) ->toBe(['Stale in progress']) ->and($service->lastWorkflowActivityAt($issues->firstOrFail())?->toIso8601String()) ->toBe(CarbonImmutable::now()->subDays(10)->toIso8601String()) ->and($summary) ->toBe([ 'unique_issue_count' => 1, 'broken_assignment_count' => 0, 'stale_in_progress_count' => 1, ]); }); it('counts multi-reason findings once while excluding healthy assigned work and ordinary intake backlog', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 22, 9, 0, 0, 'UTC')); [$service, $viewer, $workspace, $tenant] = assignmentHygieneServiceContext(); $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(); $brokenAndStale = assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $lostMember->getKey(), 'status' => Finding::STATUS_IN_PROGRESS, 'in_progress_at' => now()->subDays(8), 'subject_external_id' => 'broken-and-stale', ]); recordAssignmentHygieneWorkflowAudit($brokenAndStale, AuditActionId::FindingInProgress->value, CarbonImmutable::now()->subDays(8)); assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => (int) $viewer->getKey(), 'status' => Finding::STATUS_TRIAGED, 'subject_external_id' => 'healthy-assigned', ]); assignmentHygieneFinding($tenant, [ 'owner_user_id' => (int) $viewer->getKey(), 'assignee_user_id' => null, 'status' => Finding::STATUS_NEW, 'subject_external_id' => 'ordinary-intake', ]); $issues = $service->issueQuery($workspace, $viewer)->get(); $summary = $service->summary($workspace, $viewer); expect($issues)->toHaveCount(1) ->and((int) $issues->firstOrFail()->getKey())->toBe((int) $brokenAndStale->getKey()) ->and($service->reasonLabelsFor($issues->firstOrFail())) ->toBe(['Broken assignment', 'Stale in progress']) ->and($summary) ->toBe([ 'unique_issue_count' => 1, 'broken_assignment_count' => 1, 'stale_in_progress_count' => 1, ]); });