ExternalBalanceRefresher::resetLivenessProbes()); function stubBalanceProvidersOk(): void { // 🔴 Заглушка на ВСЕ внешние обращения. Пробы живости, которые тест не подменяет // (yookassa / self_render / sales_finder), ходили в настоящую сеть: ответ живого // мира решал, покраснеет тест или нет. Отсюда «падает в общем прогоне, проходит // в одиночку» — хвост Х4, тянувшийся с 30.07.2026. Пустой ответ 200 = «сервис жив», // а красным в каждом тесте делается ровно одна нужная проба через useLivenessProbes(). Http::fake(); app()->instance(DadataBalanceProvider::class, fakeProvider('dadata', BalanceReading::ok('dadata', 9000, 'RUB', 100))); app()->instance(SupplierBalanceProvider::class, fakeProvider('supplier', BalanceReading::ok('supplier', 90000, 'RUB', null))); app()->instance(YandexCloudBalanceProvider::class, fakeProvider('yandex_cloud', BalanceReading::ok('yandex_cloud', 90000, 'RUB', 100))); // captcha — единственная проба, которую эти тесты подменяют через useLivenessProbes(); // остальные пробы (yookassa/self_render/sales_finder) идут в реальные классы без Http::fake(). // self_render по умолчанию не настроен (grey, без сети). sales_finder же по умолчанию // смотрит на боевой домен (config/services.php) — обнуляем его здесь, иначе тест бьёт // по сети и красный ответ ломает ожидание «письмо не ушло». config()->set('services.sales_finder.base_url', ''); } it('шлёт одно письмо, когда сервис впервые покраснел', function () { Mail::fake(); stubBalanceProvidersOk(); ExternalBalanceRefresher::useLivenessProbes([ fakeProbe('captcha', LivenessReading::down('captcha', 'HTTP 500')), ]); (new RefreshExternalBalancesJob)->handle(app(ExternalBalanceRefresher::class)); Mail::assertSent(ExternalServiceDownMail::class, 1); }); it('не шлёт письмо, если сервис уже был красным (без спама)', function () { Mail::fake(); stubBalanceProvidersOk(); // Предзаливаем строку: captcha уже красный. DB::connection('pgsql_supplier')->table('external_service_balances')->updateOrInsert( ['service_key' => 'captcha'], ['light' => 'red', 'ok' => true, 'currency' => 'RUB', 'checked_at' => now(), 'updated_at' => now()], ); ExternalBalanceRefresher::useLivenessProbes([ fakeProbe('captcha', LivenessReading::down('captcha', 'HTTP 500')), ]); (new RefreshExternalBalancesJob)->handle(app(ExternalBalanceRefresher::class)); Mail::assertNothingSent(); }); it('не шлёт письмо, когда всё зелёное', function () { Mail::fake(); stubBalanceProvidersOk(); ExternalBalanceRefresher::useLivenessProbes([ fakeProbe('captcha', LivenessReading::alive('captcha', 'ок')), ]); (new RefreshExternalBalancesJob)->handle(app(ExternalBalanceRefresher::class)); Mail::assertNothingSent(); });