3fdcd88ad3
Задача 16 закрыта решением владельца. Дороги «робот везёт документ в кабинет Яндекса» не существует — доказано двумя нарочными отказами, обычной тематикой и лицензируемой: в окне отказа ноль полей для файла, документы Яндекс принимает только снаружи кабинета. Приём документа оставлен, но портал больше не молчит: клиент приложил файл — в ленту сразу ложится отметка «документ у нас, передать его Яндексу автоматически нельзя, при необходимости отнесём сами и напишем здесь», а владельцу уходит письмо на адрес алертов. Без письма обещание было бы пустым: файл просто лёг бы на диск. Сам файл письмом не отправляем — это чужие бумаги. Обычный ответ без файла ни отметки, ни письма не даёт. Экран «ждёт разбора» в админке: ручка была, экрана не было. Третья карточка на странице «Реклама» — клиент, кампания, что робот делал человеческими словами, номер объявления и на чём споткнулся. В подписи прямо сказано, чего там НЕ будет: обычных отказов, их клиент разбирает сам. Дальше — разбор собственной работы этого дня. Найдено четыре ошибки, все исправлены. 1. ТЯЖЁЛАЯ. Доклад разведки на бою уронил бы очередь заданий целиком. Робот пишет в ленту под служебной ролью, а у неё на этой таблице было только чтение. Отказ по правам, 500 роботу, три повтора — и задание навсегда «в работе». Пока хоть одно задание в работе, выдача отвечает «работы нет» ВСЕМ клиентам. Лечение — запись схемы v9.17: право на запись плюс нумератор. В плане про это было написано прямым текстом, я прошёл мимо. Тесты поймать не могли: ходят суперпользователем. 2. Признак «набор создан» я выдумал: взял метку, которая в нашей же разметке описана как СКРЫТАЯ галочка. Проверка «видно ли её» не сработала бы никогда. Признак с экрана убран совсем: успех определяет портал слепком креативов, а «окно не закрылось» — это норма, так и есть живьём. 3. Сломал ленту для повторного отказа. Поменял защиту от дублей на «такой текст уже когда-либо был» — и клиент, починивший рекламу и получивший тот же отказ, не увидел бы ничего. Вернул сравнение с последним сообщением, а заглушку «причину выясняем» держит теперь сам джоб: показываем один раз, пока сказать нечего. 4. Мой собственный тест оказался пустышкой: оставался зелёным при вырезанной защите. В нём отклонялись ВСЕ объявления, а тогда кампания уходит в «отклонена» и обход её больше не берёт. Сценарий существует только при частичном отказе — тест переписан на два объявления и теперь вырезание защиты его роняет. Портал 391/391, админские экраны 9/9, фронт на затронутых наборах 52/52, робот 74/74, мест снятия заморозки денег по-прежнему четыре. На боевой не выкатывалось, рубильник Директа выключен, живьём разведка не гонялась. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
174 lines
8.9 KiB
PHP
174 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AdCampaign;
|
|
use App\Models\AdCampaignBanner;
|
|
use App\Models\AdCampaignMessage;
|
|
use App\Models\Tenant;
|
|
use App\Services\Advertising\CampaignMessageService;
|
|
|
|
function messageCampaign(): array
|
|
{
|
|
$tenant = Tenant::factory()->create();
|
|
$campaign = AdCampaign::create([
|
|
'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false,
|
|
]);
|
|
|
|
return [$tenant, $campaign];
|
|
}
|
|
|
|
it('текст сообщения хранится целиком, длиннее 255 знаков в том числе', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
|
|
// Модератор перечисляет претензии списком — 255 знаков не хватает. Именно предел
|
|
// колонки moderation_reason уже валил обход модерации целиком.
|
|
$long = str_repeat('Причина отказа очень подробная. ', 40);
|
|
|
|
$msg = AdCampaignMessage::create([
|
|
'tenant_id' => $tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'author' => AdCampaignMessage::AUTHOR_YANDEX,
|
|
'body' => $long,
|
|
]);
|
|
|
|
expect(mb_strlen($msg->refresh()->body))->toBe(mb_strlen($long));
|
|
});
|
|
|
|
it('пояснение Яндекса ложится в ленту', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
|
|
app(CampaignMessageService::class)->postFromYandex($campaign, null, 'Изображение не соответствует требованиям');
|
|
|
|
$messages = AdCampaignMessage::where('campaign_id', $campaign->id)->get();
|
|
expect($messages)->toHaveCount(1)
|
|
->and($messages->first()->author)->toBe(AdCampaignMessage::AUTHOR_YANDEX)
|
|
->and($messages->first()->body)->toBe('Изображение не соответствует требованиям');
|
|
});
|
|
|
|
/**
|
|
* Опрос модерации бежит по расписанию. Пока Яндекс не передумал, он присылает ОДНУ И ТУ ЖЕ
|
|
* причину при каждом обходе. Без защиты лента за сутки превратится в сотню одинаковых строк,
|
|
* и клиенту прилетит сотня писем.
|
|
*/
|
|
it('тот же текст по тому же баннеру второй раз в ленту не попадает', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
$banner = AdCampaignBanner::create([
|
|
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
|
|
'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true,
|
|
'path' => 'ad-banners/a/300x250.jpg',
|
|
]);
|
|
|
|
$service = app(CampaignMessageService::class);
|
|
$service->postFromYandex($campaign, $banner->id, 'Одно и то же');
|
|
$second = $service->postFromYandex($campaign, $banner->id, 'Одно и то же');
|
|
|
|
expect($second)->toBeNull()
|
|
->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(1);
|
|
});
|
|
|
|
/** Другой размер блока — другая беда, её надо показать отдельно. */
|
|
it('тот же текст по ДРУГОМУ баннеру в ленту попадает', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
$slot = [
|
|
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
|
|
'bytes' => 1000, 'included' => true,
|
|
];
|
|
$a = AdCampaignBanner::create($slot + ['width' => 300, 'height' => 250, 'path' => 'ad-banners/a/300x250.jpg']);
|
|
$b = AdCampaignBanner::create($slot + ['width' => 728, 'height' => 90, 'path' => 'ad-banners/a/728x90.jpg']);
|
|
|
|
$service = app(CampaignMessageService::class);
|
|
$service->postFromYandex($campaign, $a->id, 'Одно и то же');
|
|
$service->postFromYandex($campaign, $b->id, 'Одно и то же');
|
|
|
|
expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(2);
|
|
});
|
|
|
|
/** Яндекс передумал и написал другое — это новость, её показываем. */
|
|
it('изменившийся текст по тому же баннеру в ленту попадает', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
$banner = AdCampaignBanner::create([
|
|
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
|
|
'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true,
|
|
'path' => 'ad-banners/a/300x250.jpg',
|
|
]);
|
|
|
|
$service = app(CampaignMessageService::class);
|
|
$service->postFromYandex($campaign, $banner->id, 'Первая претензия');
|
|
$service->postFromYandex($campaign, $banner->id, 'Вторая претензия');
|
|
|
|
expect(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(2);
|
|
});
|
|
|
|
it('пустой текст в ленту не кладём', function () {
|
|
[, $campaign] = messageCampaign();
|
|
|
|
expect(app(CampaignMessageService::class)->postFromYandex($campaign, null, ' '))->toBeNull()
|
|
->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0);
|
|
});
|
|
|
|
/**
|
|
* 🪤 Ловушка, которую открыла разведка, и которую нельзя чинить дедупом по всей истории.
|
|
*
|
|
* Клиент починил рекламу, отправил заново — и получил ТОТ ЖЕ отказ. Это новость: клиент
|
|
* должен узнать, что его опять не пустили, и по той же причине. Дедуп «такой текст уже
|
|
* когда-то был» съел бы это сообщение, и клиент остался бы без объяснения.
|
|
*
|
|
* Поэтому защита от дублей смотрит только на ПОСЛЕДНЕЕ сообщение, а «заглушку» держит
|
|
* не она, а джоб модерации — см. SyncCampaignModerationJobTest.
|
|
*/
|
|
it('тот же отказ после доклада разведки клиент увидит снова', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
$banner = AdCampaignBanner::create([
|
|
'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id,
|
|
'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true,
|
|
'path' => 'ad-banners/a/300x250.jpg',
|
|
]);
|
|
|
|
$service = app(CampaignMessageService::class);
|
|
$service->postFromRobot($campaign, $banner->id, 'Нет предупреждения: финансовые услуги');
|
|
$service->postFromYandex($campaign, $banner->id, 'Отклонено');
|
|
$again = $service->postFromRobot($campaign, $banner->id, 'Нет предупреждения: финансовые услуги');
|
|
|
|
expect($again)->not->toBeNull()
|
|
->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(3);
|
|
});
|
|
|
|
/** Доклад разведки умеет нести снимок экрана — он и есть доказательство прочитанного. */
|
|
it('доклад разведки кладётся со снимком экрана', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
|
|
$message = app(CampaignMessageService::class)->postFromRobot($campaign, null, 'Причина', [
|
|
'path' => 'ad-messages/1/1/okno.png',
|
|
'name' => 'okno.png',
|
|
'size' => 1024,
|
|
'mime' => 'image/png',
|
|
]);
|
|
|
|
expect($message->file_name)->toBe('okno.png')
|
|
->and($message->file_path)->toBe('ad-messages/1/1/okno.png')
|
|
->and($message->author)->toBe(AdCampaignMessage::AUTHOR_YANDEX);
|
|
});
|
|
|
|
/** Пустой доклад — не доклад. В ленту не кладём. */
|
|
it('пустой доклад разведки в ленту не попадает', function () {
|
|
[, $campaign] = messageCampaign();
|
|
|
|
expect(app(CampaignMessageService::class)->postFromRobot($campaign, null, ' '))->toBeNull()
|
|
->and(AdCampaignMessage::where('campaign_id', $campaign->id)->count())->toBe(0);
|
|
});
|
|
|
|
/** Признак «Яндекс уже что-то сказал» — на нём держится защита от повторной заглушки. */
|
|
it('признак «слова Яндекса уже есть» считается по баннеру, а не по кампании целиком', function () {
|
|
[$tenant, $campaign] = messageCampaign();
|
|
$slot = ['tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'bytes' => 1000, 'included' => true];
|
|
$a = AdCampaignBanner::create($slot + ['width' => 300, 'height' => 250, 'path' => 'ad-banners/a/300x250.jpg']);
|
|
$b = AdCampaignBanner::create($slot + ['width' => 728, 'height' => 90, 'path' => 'ad-banners/a/728x90.jpg']);
|
|
|
|
$service = app(CampaignMessageService::class);
|
|
$service->postFromYandex($campaign, $a->id, 'Отклонено');
|
|
|
|
expect($service->hasFromYandex($campaign, $a->id))->toBeTrue()
|
|
->and($service->hasFromYandex($campaign, $b->id))->toBeFalse();
|
|
});
|