create([ 'workspace_id' => $workspaceId, 'is_enabled' => true, ]); $rule = AlertRule::factory()->create([ 'workspace_id' => $workspaceId, 'event_type' => 'baseline_high_drift', 'minimum_severity' => 'low', 'is_enabled' => true, 'cooldown_seconds' => $cooldownSeconds, ]); $rule->destinations()->attach($destination->getKey(), [ 'workspace_id' => $workspaceId, ]); return [$rule, $destination]; } /** * @return array> */ function invokeBaselineHighDriftEvents(int $workspaceId, CarbonImmutable $windowStart): array { $job = new EvaluateAlertsJob($workspaceId); $reflection = new ReflectionMethod($job, 'baselineHighDriftEvents'); /** @var array> $events */ $events = $reflection->invoke($job, $workspaceId, $windowStart); return $events; } it('produces baseline drift events only for new and reopened baseline findings that meet the workspace threshold', function (): void { $now = CarbonImmutable::parse('2026-02-28T12:00:00Z'); CarbonImmutable::setTestNow($now); [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) session()->get(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY); $windowStart = $now->subHour(); WorkspaceSetting::query()->create([ 'workspace_id' => $workspaceId, 'domain' => 'baseline', 'key' => 'alert_min_severity', 'value' => Finding::SEVERITY_HIGH, 'updated_by_user_id' => (int) $user->getKey(), ]); $newFinding = Finding::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'source' => 'baseline.compare', 'fingerprint' => 'baseline-fingerprint-new', 'severity' => Finding::SEVERITY_CRITICAL, 'status' => Finding::STATUS_NEW, 'created_at' => $now->subMinutes(10), 'evidence_jsonb' => ['change_type' => 'missing_policy'], ]); $reopenedFinding = Finding::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'source' => 'baseline.compare', 'fingerprint' => 'baseline-fingerprint-reopened', 'severity' => Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_REOPENED, 'reopened_at' => $now->subMinutes(5), 'evidence_jsonb' => ['change_type' => 'different_version'], ]); Finding::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'source' => 'baseline.compare', 'fingerprint' => 'baseline-too-old', 'severity' => Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_NEW, 'created_at' => $now->subDays(1), 'evidence_jsonb' => ['change_type' => 'missing_policy'], ]); Finding::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'source' => 'baseline.compare', 'fingerprint' => 'baseline-below-threshold', 'severity' => Finding::SEVERITY_MEDIUM, 'status' => Finding::STATUS_NEW, 'created_at' => $now->subMinutes(5), 'evidence_jsonb' => ['change_type' => 'unexpected_policy'], ]); Finding::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'source' => 'permission_check', 'fingerprint' => 'not-baseline', 'severity' => Finding::SEVERITY_CRITICAL, 'status' => Finding::STATUS_NEW, 'created_at' => $now->subMinutes(5), 'evidence_jsonb' => ['change_type' => 'missing_policy'], ]); $events = invokeBaselineHighDriftEvents($workspaceId, $windowStart); expect($events)->toHaveCount(2); $eventsByFindingId = collect($events)->keyBy(static fn (array $event): int => (int) $event['metadata']['finding_id']); expect($eventsByFindingId[$newFinding->getKey()]) ->toMatchArray([ 'event_type' => 'baseline_high_drift', 'tenant_id' => (int) $tenant->getKey(), 'severity' => Finding::SEVERITY_CRITICAL, 'fingerprint_key' => 'finding_fingerprint:baseline-fingerprint-new', 'metadata' => [ 'finding_id' => (int) $newFinding->getKey(), 'finding_fingerprint' => 'baseline-fingerprint-new', 'change_type' => 'missing_policy', ], ]); expect($eventsByFindingId[$reopenedFinding->getKey()]) ->toMatchArray([ 'event_type' => 'baseline_high_drift', 'tenant_id' => (int) $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, 'fingerprint_key' => 'finding_fingerprint:baseline-fingerprint-reopened', 'metadata' => [ 'finding_id' => (int) $reopenedFinding->getKey(), 'finding_fingerprint' => 'baseline-fingerprint-reopened', 'change_type' => 'different_version', ], ]); }); it('uses the finding fingerprint for dedupe and remains cooldown compatible', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) session()->get(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY); [$rule, $destination] = createBaselineHighDriftRuleWithDestination($workspaceId, cooldownSeconds: 3600); $finding = Finding::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'source' => 'baseline.compare', 'fingerprint' => 'stable-fingerprint-key', 'severity' => Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_NEW, 'evidence_jsonb' => ['change_type' => 'missing_policy'], ]); $event = [ 'event_type' => 'baseline_high_drift', 'tenant_id' => (int) $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, 'fingerprint_key' => 'finding_fingerprint:stable-fingerprint-key', 'title' => 'Baseline drift detected', 'body' => 'A baseline finding was created.', 'metadata' => [ 'finding_id' => (int) $finding->getKey(), 'finding_fingerprint' => 'stable-fingerprint-key', 'change_type' => 'missing_policy', ], ]; $workspace = Workspace::query()->findOrFail($workspaceId); $dispatchService = app(AlertDispatchService::class); expect($dispatchService->dispatchEvent($workspace, $event))->toBe(1); expect($dispatchService->dispatchEvent($workspace, $event))->toBe(1); $deliveries = AlertDelivery::query() ->where('workspace_id', $workspaceId) ->where('alert_rule_id', (int) $rule->getKey()) ->where('alert_destination_id', (int) $destination->getKey()) ->where('event_type', 'baseline_high_drift') ->orderBy('id') ->get(); expect($deliveries)->toHaveCount(2); expect($deliveries[0]->status)->toBe(AlertDelivery::STATUS_QUEUED); expect($deliveries[1]->status)->toBe(AlertDelivery::STATUS_SUPPRESSED); });