Files
portal/app/tests/Feature/Admin/AdminRobotStuckTest.php
T
Дмитрий 0ba7890bf9 feat реклама за показы: робот-разведчик приносит клиенту причину отказа с экрана кабинета
Задача 15. Программный интерфейс Яндекса причину отказа не отдаёт — на отклонённое
объявление приходит «Отклонено на модерации.» и всё. Причина висит только на экране
кабинета, и добыть её может лишь тот, у кого есть глаза.

Как теперь работает:
опрос модерации видит отказ и ставит роботу задание разведки по этому объявлению;
робот открывает список объявлений, находит ячейку своего объявления, кликает по статусу,
раскрывает строку причины, читает текст и снимает одно окно; доклад уезжает порталу формой
вместе со снимком; портал кладёт его в ленту от имени Яндекса слово в слово, клиенту
письмо и колокольчик. Робот не понял, что видит — задание сбойное, владельцу письмо,
в ленту клиенту ничего не сочиняем. В админке появилась ручка «ждёт разбора».

Четыре ловушки, пойманные по дороге и проверенные вырезанием:

1. Дедуп разведки нельзя вешать на кампанию. Отказ никуда не девается, а обход бежит
   по расписанию: после закрытия первой разведки поставилась бы вторая, и робот ходил бы
   в кабинет по кругу. Ключ — номер объявления, журнал схемы v9.16.
2. Рубильник держал не выдачу задания, а построение клиента Директа. Разведке слепок
   креативов не нужен, значит при выключенном рубильнике она получила бы задание,
   и робот пошёл бы в живой кабинет.
3. Дедуп ленты сравнивал только с последним сообщением Яндекса. После доклада робота
   обход снова клал бы «причину выясняем» поверх настоящей причины.
4. Постановка разведки шла без tenant-контекста — на бою она не сработала бы ВООБЩЕ
   и молча: поиск дубля давал бы ноль, запись падала бы на политике доступа, всё это
   в предупреждение журнала при зелёных тестах. Поймал rls-reviewer. Лечение — своя
   транзакция с выставлением клиента, рецепт ChargeCampaignSpendJob. Сторож поставлен
   на сам механизм: обычным тестом это не ловится, они ходят суперпользователем.

Заодно: разведке больше не снимается слепок креативов — лишний поход в живой Яндекс
внутри открытой транзакции.

Портал 382/382, робот 75/75, мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 19:28:31 +03:00

84 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AdCampaign;
use App\Models\AdCampaignBanner;
use App\Models\AdCreativeJob;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
/**
* Список «ждёт разбора»: кампании, где робот сходил в кабинет и НЕ ПОНЯЛ, что видит.
*
* Это не про обычные отказы — их клиент разбирает сам, по причине в переписке. Это про
* сбой цепочки: разметка кабинета поменялась, страница не открылась, объявления в списке
* не нашлось. Без такого списка цепочка встанет молча: клиент будет ждать ответа, которого
* не будет, и никто об этом не узнает.
*/
function stuckSetup(string $status, string $kind = AdCreativeJob::KIND_INSPECT): array
{
$tenant = Tenant::factory()->create(['organization_name' => 'Окна Москва']);
$campaign = AdCampaign::create([
'tenant_id' => $tenant->id,
'name' => 'Кампания',
'status' => AdCampaign::STATUS_REJECTED,
'audience_days' => 10,
'budget_rub' => '500.00',
]);
$banner = AdCampaignBanner::create([
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true,
'path' => "ad-banners/{$tenant->id}/{$campaign->id}/300x250.jpg",
'yandex_ad_id' => 17787055204,
]);
$job = AdCreativeJob::create([
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
'kind' => $kind, 'status' => $status,
'yandex_ad_id' => $banner->yandex_ad_id,
'failure_reason' => 'разметка не совпала: окна BannerStatusCell.Popup нет',
'finished_at' => now(),
]);
return [$tenant, $campaign, $job];
}
test('в списке ждёт разбора видно кампанию, где разведка сорвалась', function () {
[$tenant, $campaign] = stuckSetup(AdCreativeJob::STATUS_FAILED);
$r = $this->getJson('/api/admin/advertising/robot-stuck');
$r->assertStatus(200);
$row = collect($r->json('data'))->firstWhere('campaign_id', $campaign->id);
expect($row)->not->toBeNull()
->and($row['tenant_name'])->toBe('Окна Москва')
->and($row['failure_reason'])->toContain('разметка не совпала')
->and((int) $row['yandex_ad_id'])->toBe(17787055204);
});
/** Удачная разведка разбора не ждёт — она уже всё рассказала клиенту в переписке. */
test('удачная разведка в список ждёт разбора не попадает', function () {
[, $campaign] = stuckSetup(AdCreativeJob::STATUS_DONE);
$r = $this->getJson('/api/admin/advertising/robot-stuck');
expect(collect($r->json('data'))->firstWhere('campaign_id', $campaign->id))->toBeNull();
});
/**
* Сорванная ЗАЛИВКА картинок — тоже сбой робота, и владельцу её видеть надо: кампания
* стоит и не запускается. Список один на все виды заданий.
*/
test('сорванная заливка картинок в списке тоже видна', function () {
[, $campaign] = stuckSetup(AdCreativeJob::STATUS_FAILED, AdCreativeJob::KIND_UPLOAD);
$r = $this->getJson('/api/admin/advertising/robot-stuck');
expect(collect($r->json('data'))->firstWhere('campaign_id', $campaign->id))->not->toBeNull();
});