337f6b9897
Дата старта (решение владельца 06.08.2026). Портал не передаёт кабинету МТС ни одной даты — их ставит кабинет своими умолчаниями. Живой прогон показал: старт оказался ЗАВТРАШНИМ, тогда как экран обещал клиенту показы «7 дней», подразумевая сегодня. Робот читает день начала из ТОЙ ЖЕ строки списка, куда и так ходит за вердиктом — ни одного лишнего захода в кабинет; портал хранит его в client_tg_campaigns.starts_on и показывает клиенту «Показы начнутся 7 августа». Проверено на ЖИВОМ кабинете: три задания подряд вернули startDate 2026-08-07 по кампании МТС 2237821. Мастер перестал молчать о том, что день начала ставит кабинет, а не мы. Ф-2, карточка приёмки Т-Ф4. У кампаний в движении видно «Проверяли 5 минут назад». Считаются только ЗАКОНЧЕННЫЕ проверки, включая неудачные: задание в очереди работой не является, а неудачная проверка — всё равно признак жизни. Именно в такой тишине владелец 36 часов не знал, что робот вообще не может войти в кабинет. Ф-3. Имена полей в ошибках формы по-русски: «Лимит на объявление не может быть меньше 1 ₽» вместо «Поле budget cap rub должно быть не меньше 1». Серая кнопка «Запустить» называет причину и шаг, куда вернуться, а не гаснет молча. Карточки Т-Р3 и Т-Р4 закрыты тестами (живьём не воспроизвести). Попутно найдено: обе защиты, стерегущие ЕДИНСТВЕННОЕ место траты живых денег роботом, были без единого теста. Сторожа доказаны вырезанием. Тесты: ClientTg 378 зелёных, экраны 2158, робот 197. Статанализ 0, стиль 0, типы чисто. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
155 lines
6.8 KiB
PHP
155 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ClientTg\Campaign;
|
|
use App\Models\ClientTg\RobotJob;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Дефект Ф-2 приёмки 06.08.2026, карточка ранбука Т-Ф4 — не видно, жива ли система.
|
|
*
|
|
* 🔴 Кампания может неделями висеть «На модерации», и клиент никак не отличит работу от
|
|
* поломки: портал не отдавал ему ни времени последнего опроса робота, ни времени
|
|
* последней проверки вердикта. Именно в такой тишине владелец 36 часов не знал, что
|
|
* робот вообще не может войти в кабинет.
|
|
*
|
|
* Отдаём время ПОСЛЕДНЕЙ ЗАКОНЧЕННОЙ проверки. Не «поставлено в очередь» — задание
|
|
* могло быть поставлено и не выполниться ни разу; такое «мы работаем» было бы враньём.
|
|
*/
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenant->id);
|
|
$this->actingAs($this->user);
|
|
});
|
|
|
|
function kampaniyaNaModeracii(int $tenantId): Campaign
|
|
{
|
|
return Campaign::query()->create([
|
|
'tenant_id' => $tenantId,
|
|
'status' => Campaign::STATUS_MODERATING,
|
|
'ad_text' => 'Приходите к нам',
|
|
'ad_link' => 'https://t.me/x',
|
|
'ord_category' => 'Размещение рекламы',
|
|
'budget_cap_rub' => '500.00',
|
|
'audience_kind' => Campaign::AUDIENCE_LIST,
|
|
'planned_count' => 400,
|
|
'estimated_cost_rub' => '268.80',
|
|
'mts_campaign_id' => '2237821',
|
|
'created_by' => 1,
|
|
]);
|
|
}
|
|
|
|
it('клиент видит время последней ЗАКОНЧЕННОЙ проверки робота', function () {
|
|
$campaign = kampaniyaNaModeracii($this->tenant->id);
|
|
|
|
RobotJob::query()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'mode' => RobotJob::MODE_READ_STATUS,
|
|
'payload' => ['mode' => 'read-status'],
|
|
'status' => RobotJob::STATUS_DONE,
|
|
'finished_at' => '2026-08-06 14:31:00',
|
|
]);
|
|
|
|
$this->getJson('/api/telegram/campaigns')
|
|
->assertOk()
|
|
->assertJsonPath('campaigns.0.last_robot_check_at', '2026-08-06T14:31:00.000000Z');
|
|
});
|
|
|
|
// 🪤 Провалившаяся проверка — ТОЖЕ проверка: система живая, просто кабинет не ответил.
|
|
// Показывать только успешные значило бы молчать ровно в тот момент, когда что-то не так.
|
|
it('неудачная проверка тоже считается — система работала', function () {
|
|
$campaign = kampaniyaNaModeracii($this->tenant->id);
|
|
|
|
RobotJob::query()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'mode' => RobotJob::MODE_READ_STATUS,
|
|
'payload' => ['mode' => 'read-status'],
|
|
'status' => RobotJob::STATUS_FAILED,
|
|
'finished_at' => '2026-08-06 15:00:00',
|
|
'failure_reason' => 'Список кампаний не отрисовался',
|
|
]);
|
|
|
|
$this->getJson('/api/telegram/campaigns')
|
|
->assertOk()
|
|
->assertJsonPath('campaigns.0.last_robot_check_at', '2026-08-06T15:00:00.000000Z');
|
|
});
|
|
|
|
// 🪤 Задание в очереди — ещё НЕ проверка. Показать по нему «проверяли» значило бы
|
|
// обещать работу, которой не было: именно так выглядели 36 часов молчания.
|
|
it('задание только поставлено в очередь — это ещё не проверка', function () {
|
|
$campaign = kampaniyaNaModeracii($this->tenant->id);
|
|
|
|
RobotJob::query()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'mode' => RobotJob::MODE_READ_STATUS,
|
|
'payload' => ['mode' => 'read-status'],
|
|
'status' => RobotJob::STATUS_QUEUED,
|
|
'finished_at' => null,
|
|
]);
|
|
|
|
$this->getJson('/api/telegram/campaigns')
|
|
->assertOk()
|
|
->assertJsonPath('campaigns.0.last_robot_check_at', null);
|
|
});
|
|
|
|
it('из нескольких проверок показывается САМАЯ СВЕЖАЯ', function () {
|
|
$campaign = kampaniyaNaModeracii($this->tenant->id);
|
|
|
|
foreach (['2026-08-06 12:00:00', '2026-08-06 16:45:00', '2026-08-06 14:00:00'] as $kogda) {
|
|
RobotJob::query()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'campaign_id' => $campaign->id,
|
|
'mode' => RobotJob::MODE_READ_STATUS,
|
|
'payload' => ['mode' => 'read-status'],
|
|
'status' => RobotJob::STATUS_DONE,
|
|
'finished_at' => $kogda,
|
|
]);
|
|
}
|
|
|
|
$this->getJson('/api/telegram/campaigns')
|
|
->assertOk()
|
|
->assertJsonPath('campaigns.0.last_robot_check_at', '2026-08-06T16:45:00.000000Z');
|
|
});
|
|
|
|
it('робота ещё не звали — поле пустое, а не выдуманное', function () {
|
|
kampaniyaNaModeracii($this->tenant->id);
|
|
|
|
$this->getJson('/api/telegram/campaigns')
|
|
->assertOk()
|
|
->assertJsonPath('campaigns.0.last_robot_check_at', null);
|
|
});
|
|
|
|
// 🔴 Чужие проверки в свою карточку не пускаем: это тот же класс дыры, что и чужие
|
|
// кампании. Время чужой работы — не наше дело и не наш признак живости.
|
|
it('проверка ЧУЖОЙ кампании в карточку не попадает', function () {
|
|
$moya = kampaniyaNaModeracii($this->tenant->id);
|
|
|
|
$chuzhoy = Tenant::factory()->create();
|
|
DB::statement('SET LOCAL app.current_tenant_id = '.$chuzhoy->id);
|
|
$chuzhaya = kampaniyaNaModeracii($chuzhoy->id);
|
|
RobotJob::query()->create([
|
|
'tenant_id' => $chuzhoy->id,
|
|
'campaign_id' => $chuzhaya->id,
|
|
'mode' => RobotJob::MODE_READ_STATUS,
|
|
'payload' => ['mode' => 'read-status'],
|
|
'status' => RobotJob::STATUS_DONE,
|
|
'finished_at' => '2026-08-06 23:59:00',
|
|
]);
|
|
DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenant->id);
|
|
|
|
$this->getJson('/api/telegram/campaigns')
|
|
->assertOk()
|
|
->assertJsonPath('campaigns.0.id', $moya->id)
|
|
->assertJsonPath('campaigns.0.last_robot_check_at', null);
|
|
});
|