Files
portal/app/tests/Feature/ClientTg/SchyotchikZavisshejModeraciiTest.php
T
Дмитрий 6b732de6ad fix телеграм: счётчик зависшей модерации сбрасывался каждой проверкой робота
Кампания, застрявшая на модерации дольше 48 часов, обязана уйти в ручной
разбор — иначе висит вечно с замороженными деньгами клиента и молча.
Срок считается по времени изменения строки, а вчерашняя правка «день начала
показов» писала дату при КАЖДОЙ проверке робота массовым обновлением, которое
всегда двигает updated_at. Робот ходит раз в четверть часа — счётчик не
досчитал бы никогда. Защита была цела на вид и мертва на деле.

Улика с боевого: у кампании №15 время изменения строки совпадало с концом
последнего задания робота до секунды, при том что вердикта не было третьи сутки.

Пишем дату, только если она вправду изменилась. Сравнение сырых значений мимо
приведения типов: starts_on это date:Y-m-d, сравнение с Carbon разошлось бы молча.

Сторожа SchyotchikZavisshejModeraciiTest (3 шт) видены красными до правки.
Блок телеграма 381 зелёный на своей базе, статанализ 0, pint чисто.
На боевой НЕ выкачено.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:50:01 +03:00

116 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\ClientTg\SweepStuckTelegramCampaignsJob;
use App\Models\ClientTg\Campaign;
use App\Models\Tenant;
use App\Services\ClientTg\RobotResult;
use App\Services\ClientTg\TelegramModerationVerdictApplier;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
// Уборщик перечисляет кампании через pgsql_supplier (BYPASSRLS) — шарим PDO,
// иначе созданное на дефолтном соединении внутри транзакции ему не видно.
uses(RefreshDatabase::class, SharesSupplierPdo::class);
/**
* 🔴 Живая дыра, найденная 07.08.2026 замером боевого.
*
* Кампания, застрявшая на модерации дольше `client_tg.moderation_stuck_hours` (48 ч),
* обязана уйти в ручной разбор: иначе она висит вечно с замороженными деньгами клиента,
* и об этом никто не узнаёт. Срок уборщик считает по времени последнего изменения строки.
*
* А правка «день начала показов» (06.08.2026) пишет дату в кампанию при КАЖДОЙ проверке
* робота — массовым обновлением, которое всегда двигает `updated_at`, даже когда дата не
* изменилась. Робот ходит раз в четверть часа, значит счётчик 48 часов сбрасывается
* каждые 15 минут и не досчитывает НИКОГДА. Защита цела на вид и мертва на деле.
*
* 🔑 Улика с боевого: у кампании №15 время изменения строки совпадало с концом
* последнего задания робота до секунды, при том что вердикта не было третьи сутки.
*
* Лечение: писать дату, только если она вправду изменилась.
*/
/** Тенант с открытым tenant-контекстом (RLS). */
function moderaciyaTenant(): Tenant
{
$t = Tenant::factory()->create(['balance_rub' => '0.00']);
DB::statement('SET LOCAL app.current_tenant_id = '.$t->id);
return $t;
}
/** Кампания на модерации, состаренная на $chasovNazad часов. */
function moderaciyaKampaniya(int $tenantId, int $chasovNazad, ?string $startsOn): Campaign
{
$c = Campaign::query()->create([
'tenant_id' => $tenantId,
'status' => Campaign::STATUS_MODERATING,
'mts_campaign_id' => '2237821',
'starts_on' => $startsOn,
'ad_text' => 'Тест счётчика зависшей модерации',
'ord_category' => 'Размещение рекламы',
'budget_cap_rub' => '1000.00',
'audience_kind' => Campaign::AUDIENCE_LIST,
'planned_count' => 0,
'estimated_cost_rub' => '268.80',
'created_by' => 1,
]);
// Прямой апдейт мимо Eloquent — иначе штамп «сейчас».
DB::table('client_tg_campaigns')->where('id', $c->id)
->update(['updated_at' => now()->subHours($chasovNazad)]);
return $c;
}
/** Робот сходил в кабинет: вердикта нет, дата старта такая-то. */
function robotProveril(int $tenantId, int $campaignId, ?string $startDate): void
{
app(TelegramModerationVerdictApplier::class)->apply(
$tenantId,
$campaignId,
new RobotResult(ok: true, moderationStatus: 'moderating', startDate: $startDate),
false,
);
}
it('проверка робота не сбрасывает счётчик зависшей модерации', function () {
config()->set('client_tg.sandbox', false);
config()->set('client_tg.moderation_stuck_hours', 48);
$t = moderaciyaTenant();
$c = moderaciyaKampaniya($t->id, 49, '2026-08-07');
// Робот сходил ещё раз: вердикта по-прежнему нет, дата старта ТА ЖЕ самая.
robotProveril($t->id, $c->id, '2026-08-07');
SweepStuckTelegramCampaignsJob::dispatchSync();
// Кампания висит 49 часов — обязана уйти в разбор, сколько бы раз её ни проверяли.
expect(Campaign::find($c->id)->status)->toBe(Campaign::STATUS_NEEDS_REVIEW);
});
it('повторная запись той же даты старта не двигает время изменения строки', function () {
$t = moderaciyaTenant();
$c = moderaciyaKampaniya($t->id, 49, '2026-08-07');
$bylo = DB::table('client_tg_campaigns')->where('id', $c->id)->value('updated_at');
robotProveril($t->id, $c->id, '2026-08-07');
$stalo = DB::table('client_tg_campaigns')->where('id', $c->id)->value('updated_at');
expect($stalo)->toBe($bylo);
});
it('новая дата старта всё-таки записывается — клиент должен её увидеть', function () {
$t = moderaciyaTenant();
$c = moderaciyaKampaniya($t->id, 49, null);
robotProveril($t->id, $c->id, '2026-08-09');
expect((string) DB::table('client_tg_campaigns')->where('id', $c->id)->value('starts_on'))
->toBe('2026-08-09');
});