for($tenant)->create([ 'severity' => Finding::SEVERITY_MEDIUM, 'status' => Finding::STATUS_ACKNOWLEDGED, 'acknowledged_at' => CarbonImmutable::parse('2026-02-20T00:00:00Z'), 'acknowledged_by_user_id' => (int) $user->getKey(), 'first_seen_at' => null, 'last_seen_at' => null, 'times_seen' => null, 'sla_days' => null, 'due_at' => null, 'triaged_at' => null, 'created_at' => CarbonImmutable::parse('2026-02-10T00:00:00Z'), 'updated_at' => CarbonImmutable::parse('2026-02-10T00:00:00Z'), ]); BackfillFindingLifecycleJob::dispatchSync( tenantId: (int) $tenant->getKey(), workspaceId: (int) $tenant->workspace_id, initiatorUserId: (int) $user->getKey(), ); $finding->refresh(); expect($finding->status)->toBe(Finding::STATUS_TRIAGED) ->and($finding->triaged_at?->toIso8601String())->toBe('2026-02-20T00:00:00+00:00') ->and($finding->first_seen_at?->toIso8601String())->toBe('2026-02-10T00:00:00+00:00') ->and($finding->last_seen_at?->toIso8601String())->toBe('2026-02-10T00:00:00+00:00') ->and($finding->times_seen)->toBe(1) ->and($finding->sla_days)->toBe(14) ->and($finding->due_at?->toIso8601String())->toBe('2026-03-10T10:00:00+00:00') ->and($finding->acknowledged_at?->toIso8601String())->toBe('2026-02-20T00:00:00+00:00') ->and((int) $finding->acknowledged_by_user_id)->toBe((int) $user->getKey()); CarbonImmutable::setTestNow(); }); it('computes drift recurrence keys and consolidates drift duplicates', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-02-24T10:00:00Z')); [$user, $tenant] = createUserWithTenant(role: 'manager'); $scopeKey = hash('sha256', 'scope-drift-backfill-duplicate'); $evidence = [ 'change_type' => 'modified', 'summary' => [ 'kind' => 'policy_snapshot', 'changed_fields' => ['snapshot_hash'], ], 'baseline' => ['policy_id' => 'policy-dupe'], 'current' => ['policy_id' => 'policy-dupe'], ]; $open = Finding::factory()->for($tenant)->create([ 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'scope_key' => $scopeKey, 'subject_type' => 'policy', 'subject_external_id' => 'policy-dupe', 'status' => Finding::STATUS_NEW, 'recurrence_key' => null, 'evidence_jsonb' => $evidence, 'first_seen_at' => null, 'last_seen_at' => null, 'times_seen' => null, 'sla_days' => null, 'due_at' => null, 'created_at' => CarbonImmutable::parse('2026-02-20T00:00:00Z'), 'updated_at' => CarbonImmutable::parse('2026-02-20T00:00:00Z'), ]); $duplicate = Finding::factory()->for($tenant)->create([ 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'scope_key' => $scopeKey, 'subject_type' => 'policy', 'subject_external_id' => 'policy-dupe', 'status' => Finding::STATUS_RESOLVED, 'resolved_at' => CarbonImmutable::parse('2026-02-21T00:00:00Z'), 'resolved_reason' => 'fixed', 'recurrence_key' => null, 'evidence_jsonb' => $evidence, 'first_seen_at' => null, 'last_seen_at' => null, 'times_seen' => null, 'created_at' => CarbonImmutable::parse('2026-02-18T00:00:00Z'), 'updated_at' => CarbonImmutable::parse('2026-02-18T00:00:00Z'), ]); BackfillFindingLifecycleJob::dispatchSync( tenantId: (int) $tenant->getKey(), workspaceId: (int) $tenant->workspace_id, initiatorUserId: (int) $user->getKey(), ); $tenantId = (int) $tenant->getKey(); $expectedRecurrenceKey = hash( 'sha256', sprintf('drift:%d:%s:policy:%s:policy_snapshot:modified', $tenantId, $scopeKey, 'policy-dupe'), ); expect(Finding::query() ->where('tenant_id', $tenantId) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('recurrence_key', $expectedRecurrenceKey) ->count())->toBe(1); $open->refresh(); $duplicate->refresh(); expect($open->recurrence_key)->toBe($expectedRecurrenceKey) ->and($open->status)->toBe(Finding::STATUS_NEW); expect($duplicate->recurrence_key)->toBeNull() ->and($duplicate->status)->toBe(Finding::STATUS_RESOLVED) ->and($duplicate->resolved_reason)->toBe('consolidated_duplicate') ->and($duplicate->resolved_at?->toIso8601String())->toBe('2026-02-24T10:00:00+00:00'); CarbonImmutable::setTestNow(); });