Прогон рекламной папки оставлял в базе 44 записи. Теперь ноль, папка целиком
зелёная — 351 из 351. Полная tests/Feature: 3282 теста, 0 падений, и после неё
в базе не остаётся НИЧЕГО, кроме справочников, которые кладутся при сборке.
Два места прошлая смена считала неизлечимыми — оба вылечились, потому что
диагноз был не перепроверен:
1. CampaignBannerEndpointsTest оставлял 24 записи, больше половины всей грязи.
Он нарочно ловит отказ базы, а после отказа внутри транзакции Postgres
глушит все следующие команды. Лечение — ловить отказ в ОТДЕЛЬНОЙ точке
сохранения: DB::transaction внутри уже открытой ставит savepoint и
откатывает только его. Приёмка вырезанием: убрал уникальный ключ в
миграции — тест покраснел ровно там, где должен; вернул — зелёный.
2. AdWalletUnderRealRoleTest. В промте: «откат противопоказан, он ходит
настоящей ролью базы». Оказалось — не противопоказан, смена роли идёт по
ТОМУ ЖЕ соединению и незакоммиченные записи видны. Тут была реальная
опасность, что откат обезвредит самого сторожа, поэтому вырезание делал
отдельно: убрал у службы установку контекста клиента — оба теста
покраснели. Значит сторож сторожит по-прежнему.
Журнал вебхука оставлял 4 строки: он пишется ВТОРЫМ соединением к базе, куда
обычный откат не дотягивается. Двум файлам добавлена общая связка соединений
(SharesSupplierPdo) — папка вебхука теперь оставляет ноль.
Прежняя оценка исправлена. В промте стояло «~20 файлов оставляют по одной-две
записи». На деле после уборки восьми главных грязь оставляли ДВА файла из всей
папки: 2 и 3 записи, сумма сошлась с наблюдаемыми 5 ровно. «Нет отката» и
«гадит в базу» — разные вещи: папка Autopodbor, где без отката 16 файлов, не
оставляет ни одной записи. Мерить надо остатком, а не поиском по тексту.
Боевой код не тронут: git diff по app/app/ пуст после каждой правки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Тот же класс, что нашли во втором проверяющем на СМС: чтение денег идёт вне куска
с контекстом клиента. Проверил свой модуль целиком по этому признаку.
Дыра одна, в самом больном месте. Джоб проверки модерации кампании перечисляет
кампании через обходное соединение, но возврат заморозки зовёт кошелёк на обычном
соединении и без контекста. Защита по клиентам на таблицах рекламы устроена строго:
без контекста сравнение с пустотой, и база отдаёт ноль строк.
Чем кончалось бы на бою: Яндекс отклонил весь набор объявлений - кампания помечена
отклонённой - возврат заморозки видит "кошелька нет", честно выходит - деньги клиента
остаются замороженными навсегда. Ни ошибки, ни строки в журнале, свободный остаток
занижен, новую кампанию запустить не на что.
Живой замер на опытной базе, откатанный сразу:
суперюзером, как ходят тесты - 1
боевой ролью без контекста, как читает джоб - 0
боевой ролью с контекстом - 1
Тесты ходят суперюзером, который защиту обходит, и увидеть это физически не могут.
Починка в самом кошельке, а не у вызывающего. Тенант приходит в кошелёк явным
доводом, значит и контекст - забота кошелька, а не каждого, кто его позовёт. Так
закрыт весь класс сразу, а не один случай.
Сторож гоняет деньги ПОД БОЕВОЙ РОЛЬЮ и без контекста. Написан до починки, падал
двумя разными способами: возврат молча ничего не делал, заморозка бросала "кошелька
нет". Опасен именно молчаливый.
Обход остальных фоновых путей: перечисление кампаний, сторож зависших заданий,
сборка аудитории, постановка задания разведки, запись в ленту - у всех защита на
месте. Список получателей уведомления читается из таблицы, у которой политика мягкая:
без контекста она открыта, ноль строк не даёт.
Реклама 395 из 395 при 1251 проверке, было 393. Админка и кошелёк 25 из 25.
На боевой не выкатывалось, никуда не отправлялось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>