= threshold-single-lead failures within * the watch window should create a severity=high incident with root_cause * containing 'single-lead-storm'. */ uses(DatabaseTransactions::class); uses(SharesSupplierPdo::class); // ---------- helpers -------------------------------------------------------- /** * Insert failed_webhook_jobs rows for a given supplier_lead_id. * Uses default DB::table() (pgsql connection) — same pattern as * IncidentsWatchFailuresExpandedTest's makeFailedWebhookJobExp(). * SharesSupplierPdo ensures the command (pgsql_supplier) sees this data. */ function makeStormWebhookRows(int $supplierLeadId, int $count): void { $rows = []; for ($i = 0; $i < $count; $i++) { $rows[] = [ 'raw_payload' => json_encode(['supplier_lead_id' => $supplierLeadId]), 'exception' => 'DomainException: B1 platform does not support SMS signals', 'retry_count' => 3, 'failed_at' => now()->subMinutes(rand(1, 9))->toDateTimeString(), ]; } // Insert in chunks to stay under query size limits foreach (array_chunk($rows, 200) as $chunk) { DB::table('failed_webhook_jobs')->insert($chunk); } } /** * Ensure there is at least one active saas_admin_user (required by command). * Mirrors ensureAdminExp() pattern in IncidentsWatchFailuresExpandedTest. */ function ensureAdminForStormTest(): int { $id = DB::table('saas_admin_users')->where('is_active', true)->whereNull('deleted_at')->value('id'); if ($id !== null) { return (int) $id; } return (int) DB::table('saas_admin_users')->insertGetId([ 'email' => 'storm-watch-test@liderra.ru', 'full_name' => 'Storm Watch Test Admin', 'password_hash' => '$2y$12$placeholder', 'role' => 'dev_oncall', 'is_active' => true, 'created_at' => now(), ]); } // ---------- setup ---------------------------------------------------------- beforeEach(function (): void { ensureAdminForStormTest(); // Clean only the tables the command reads/writes. // Do NOT delete saas_admin_users (may have FK refs from other tables). DB::table('failed_webhook_jobs')->delete(); DB::table('incidents_log')->whereNull('resolved_at')->delete(); }); // ---------- tests ---------------------------------------------------------- it('detects single-lead-storm when one supplier_lead_id has >= 1000 failures in window', function (): void { makeStormWebhookRows(9999, 1001); $this->artisan('incidents:watch-failures', [ '--threshold-single-lead' => 1000, '--window' => 10, '--threshold' => 99999, // disable generic webhook spike to isolate ])->assertSuccessful(); $incident = DB::table('incidents_log') ->where('root_cause', 'LIKE', '%single-lead-storm%') ->first(); expect($incident)->not->toBeNull('should create incident for storm'); expect($incident->severity)->toBe('high'); expect($incident->root_cause)->toContain('9999'); }); it('does NOT create storm incident when failures are spread across many leads', function (): void { // 100 different supplier_lead_ids × 5 failures = 500 total, none reaches threshold for ($i = 1; $i <= 100; $i++) { makeStormWebhookRows($i, 5); } $this->artisan('incidents:watch-failures', [ '--threshold-single-lead' => 1000, '--window' => 10, '--threshold' => 99999, // disable generic webhook spike ])->assertSuccessful(); $stormIncidents = DB::table('incidents_log') ->where('root_cause', 'LIKE', '%single-lead-storm%') ->count(); expect($stormIncidents)->toBe(0, 'no storm when failures spread across leads'); }); it('uses default threshold of 1000 when --threshold-single-lead is not provided', function (): void { makeStormWebhookRows(7777, 1001); $this->artisan('incidents:watch-failures', [ '--threshold' => 99999, // disable generic webhook spike ])->assertSuccessful(); $incident = DB::table('incidents_log') ->where('root_cause', 'LIKE', '%single-lead-storm%') ->first(); expect($incident)->not->toBeNull('default threshold=1000 should detect 1001 failures'); expect($incident->severity)->toBe('high'); }); it('deduplicates: does not create duplicate storm incident within dedup window', function (): void { makeStormWebhookRows(8888, 1001); // Run twice — should only create 1 incident (dedup window default 60 min) $this->artisan('incidents:watch-failures', [ '--threshold-single-lead' => 1000, '--threshold' => 99999, ])->assertSuccessful(); $this->artisan('incidents:watch-failures', [ '--threshold-single-lead' => 1000, '--threshold' => 99999, ])->assertSuccessful(); $count = DB::table('incidents_log') ->where('root_cause', 'LIKE', '%single-lead-storm:8888%') ->count(); expect($count)->toBe(1, 'dedup should prevent duplicate incident'); });