set('client_tg.sandbox', true); }); /** Кампания-список с двумя фейковыми номерами (ПДн в тестах не используем). */ function idemQueuedCampaign(int $tenantId): Campaign { $campaign = Campaign::query()->create([ 'tenant_id' => $tenantId, 'status' => Campaign::STATUS_QUEUED, 'ad_text' => 'Приходите к нам за услугой', 'ad_link' => 'https://example.test/promo', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '1500.00', 'audience_kind' => Campaign::AUDIENCE_LIST, 'planned_count' => 2, 'estimated_cost_rub' => '0.00', 'created_by' => 1, ]); foreach (['79990000001', '79990000002'] as $phone) { CampaignPhone::query()->create([ 'tenant_id' => $tenantId, 'campaign_id' => $campaign->id, 'phone' => $phone, ]); } return $campaign; } /** Подменяет раннер моком, возвращающим $result; захватывает переданные params. */ function idemMockRobot(RobotResult $result, ?array &$captured = null): void { $mock = Mockery::mock(TelegramRobotRunner::class); $mock->shouldReceive('run')->andReturnUsing(function (array $params) use ($result, &$captured) { $captured = $params; return $result; }); app()->instance(TelegramRobotRunner::class, $mock); } it('Фаза A читает queued-кампанию с блокировкой строки (SELECT ... FOR UPDATE)', function () { $tenant = Tenant::factory()->create(); $campaign = idemQueuedCampaign($tenant->id); idemMockRobot(new RobotResult(ok: true, matched: 2)); DB::enableQueryLog(); RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id); $queries = DB::getQueryLog(); DB::disableQueryLog(); $lockedRead = collect($queries)->contains(function ($q) { $sql = strtolower($q['query']); return str_contains($sql, 'client_tg_campaigns') && str_contains($sql, 'for update'); }); expect($lockedRead)->toBeTrue(); }); it('повторный запуск (redelivery) не зовёт робота второй раз', function () { $tenant = Tenant::factory()->create(); $campaign = idemQueuedCampaign($tenant->id); $runs = 0; $mock = Mockery::mock(TelegramRobotRunner::class); $mock->shouldReceive('run')->andReturnUsing(function () use (&$runs) { $runs++; return new RobotResult(ok: true, matched: 2); }); app()->instance(TelegramRobotRunner::class, $mock); RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id); RunTelegramCampaignJob::dispatchSync($campaign->id, $tenant->id); // повтор — уже draft_ready → no-op expect($runs)->toBe(1); });