AuthFlowIntegrationTest и AuthLogCoverageTest писали регистрацию через BYPASSRLS pgsql_supplier без SharesSupplierPdo. Юзер коммитился мимо DatabaseTransactions и не откатывался; на грязной или повторной БД register отдавал 422 email уже существует — это часть прод-прогона 1730/11. Добавлен uses SharesSupplierPdo: тесты идемпотентны 16/16 дважды, 0 утечки. На свежей migrate-БД весь набор 1757 прошло 0 упало 1 skip. Разбор 11 в findings tails-doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
Хвосты сессии — разблокировка смены источника (Эпики 1–6)
✅ СТАТУС 26.06.2026: ВЫКАЧЕНО НА БОЕВОЙ liderra.ru, ФЛАГ ВКЛЮЧЁН, ТУМБЛЕР В АДМИНКЕ. Ветка влита в gitea
main(3cedf28f..fbf982e1), выкачена скриптомbin/deploy-source-edit.sh. Флагrouting_match_by_snapshot=true(фича боевая активна). Дружелюбный тумблер ВКЛ/ВЫКЛ на экране «Интеграция с поставщиком» (коммитf9f86ca0). Идёт сутки наблюдения (drift=0, сверять «Вечерней заливкой»). Деньги клиента целы (1 838 400 ₽ / 1013). Детали выката —ПИЛОТ.md(снимки 26.06 ~01:00 и ~01:25). Откат — тумблер в ВЫКЛ или флагfalse. Хвосты ниже — историческая хроника до выката.
🔍 26.06 — пост-выкатная сверка и полный прогон тестов НА ПРОДЕ
Сверка прод === gitea === локалка (байт-в-байт): клон gitea main на проде + sha256 каждого из 1105 git-файлов под app/: 0 расхождений. Локалка==gitea (один коммит). Прод собран ровно из git, правок мимо git нет. Лишние на проде только окруженческие (.env, bootstrap/cache/* — генерятся artisan optimize).
Полный прогон тестов на боевом Linux (изолированная база, живая liderra не тронута): 1730 прошло, 11 упало, 3 skip (689с, последовательно).
- Подтверждено: прежние «падения» — окружение, не баги. AutoPause (падал после 21:00 МСК — time-of-day фикстура), SchemaDelta (мой фикс),
--parallel(OOM воркеров на винде) — ВСЕ прошли на Linux. - 11 упавших — инфраструктурно-зависимые, не бизнес-логика: audit-цепочка под конкуренцией + verify-chains (tamper/incident), auth_log события, drop/create партиций по сроку, CSV-импорт Россвязи, DemoSeeder идемпотентность. Артефакты разовой тест-базы из
schema.sql(партиции/конкуренция/файлы-данные иначе, чем на эталонной dev/CI). Денежные пути (биллинг/лиды/раздача/синк/сделки/проекты/RLS) — все зелёные. - Браузерные тесты исключены (виснут на проде без мока):
tests/Browser/*, supplier-portalSupplierPortalClientReport/RtProject/SessionRefreshCommand/FormProjectChannel. NB: удалять можно только ФАЙЛЫ внутриtests/Browser/, не саму папку (phpunit.xml testsuite → «directory not found»).
Рецепт безопасного полного прогона на проде (изолированно) — память feedback-prod-full-test-isolated-db:
DROP DATABASE IF EXISTS liderra_testing WITH (FORCE)(PG16 — сам закрывает висящие коннекты) +CREATE ROLE test_runner LOGIN SUPERUSER PASSWORD ...+CREATE DATABASE liderra_testing OWNER test_runner.psql -d liderra_testing -f db/schema.sql(несёт схему + сиды suppliers/system_settings) +php artisan partitions:create-months --ahead=2(иначе аудит-INSERT падает «нет партиции» → 422).- Клон gitea в /tmp;
cp -rпрод-vendor (НЕ symlink — у symlink автозагрузчик заточен под прод-путь, классы тестов получают чужой namespace) +composer dump-autoload -o;mkdir storage/framework/{views,cache/data,sessions,testing}+ chmod 777 (иначе «valid cache path»); чистый.envстрого на liderra_testing+test_runner. - Прогон detached (
nohup ... &) — переживает обрыв ssh (квирк #109). НЕset -eвокругphp artisan test(вернёт не-ноль на падениях → оборвёт до печати итога). - Уборка: DROP базы+роли, rm клона/
.env/логов. 🚨 Грабли: широкийpkill -f artisanна проде УБИВАЕТ боевойqueue:work(systemd поднимает) — бить только по"artisan test"или/tmp/liderra-verify, не поartisan.
Дата: 25–26.06.2026. Ветка: feat/source-edit-snapshot-routing (выкачена в main 26.06).
План: docs/superpowers/plans/2026-06-25-source-edit-unblock-snapshot-routing.md.
Что СДЕЛАНО и закоммичено (все эпики 1–6 закрыты)
| Эпик | Суть | Коммит |
|---|---|---|
| 2 (ядро) | Матч источника по слепку, хвост старого источника доезжает, нет двойного списания, отложенное удаление донора | cc6c48b4 c8ec7a66 b7379e7a 35d51df6 |
| 1 | read-only поля source_locked/unlock_at/projected в ProjectResource | b1fe11b7 |
| 3 | UX: источник редактируем, баннер applies-from, диалог подтверждения (дроуэр) | 7337bd23 |
| 3 (доп) | окно «Редактировать» (NewProjectDialog) приведено к тому же поведению | 941b4b9f |
| 4 | онлайн-заморозка 18:00→00:00 (supplier_deferred_sync) + досыл 00:05 (FlushDeferredOnlineSyncJob) | 9d703ccb |
| 5 | экран отчёта вечерней заливки (supplier_sync_runs + AdminSupplierIntegrationView) | 1d18933d |
| 6.1/6.2/6.3 | ProjectRuleMessages (единый текст) + in-app уведомления + диалоги тянут текст из API | be082396 e655af62 64a76a21 |
| доп | сквозной тест потока лида для изменённого/удалённого проекта | d98fc3c8 |
| доп | объявления АКТУАЛЬНЫ по времени суток (баннер firstLeadDate + guard-сообщение computeGraceUntil) | ca923e4d |
| фикс | регрессия ProjectServiceGuardWiring (мок не ждал isProtected) | 91690c4f |
2 новые таблицы (SaaS-level без RLS как supplier_csv_reconcile_log): supplier_deferred_sync (v8.54), supplier_sync_runs (v8.55). RLS-ревью 7/7 обе. Миграции накачены на dev + liderra_testing.
Проверено глазами (Playwright): дроуэр + окно «Редактировать» (баннер 27 июня, подтверждение источника), экран «Вечерняя заливка», единый текст правил, time-aware сообщение удаления (до/после 26 июня). 0 ошибок консоли (кроме ожидаемых 422 при блоке).
Регрессия: широкий бэк 883 теста (после фикса 0 падений), целевой фронт зелёный. Полный фронт — см. долг ниже.
❗ ХВОСТЫ — что осталось
✅ Проверено ГЛАЗАМИ 26.06 (Playwright, подмена часов через page.clock)
- Баннер ДО/ПОСЛЕ 18:00 — ✅ ПОДТВЕРЖДЕНО. 13:00 МСК → «вступят в силу с 26 июня» (завтра); 19:00 МСК → «27 июня» (послезавтра). Реальный отрендеренный баннер в дроуэре. Скрин
verify-banner-before-18-26june.png. - Колокольчик (Эпик 6.2) — ✅ ПОДТВЕРЖДЕНО. После сохранения правки лимита в колокольчике сверху появилось «Изменения сохранены — Изменения вступят в силу с 26 июня. N мин назад». Скрин
verify-bell-project-rule-notification.png. NB: колокольчик грузится по 30-сек поллингу (на mount пропускается, покаauth.userне загружен — гонка с/api/auth/me), поэтому первые ~30с после полной перезагрузки пуст — давний UX-нюанс, не баг фичи.- Подчищено: тип
NotificationEvent(api/notifications.ts) и icon-map (AppTopbar.vue) не знали проproject_rule— добавлено (иконкаmdi-clock-edit-outline). Аддитивно, рантайм и так работал (fallback-иконка).
- Подчищено: тип
🔴 НАЙДЕН РЕАЛЬНЫЙ БАГ (на проде!) + ФИКС с тестом
Симптом: на проекте, по которому уже летят лиды от поставщика (isProtected), смена ТОЛЬКО лимита/региона/дней ложно блокируется ошибкой «Изменить источник можно будет после N», хотя источник не трогали.
Корень: ProjectService::update считал $sourceFieldsTouched по ПРИСУТСТВИЮ ключа signal_identifier, а дроуэр для site/call ВСЕГДА его шлёт (даже неизменённый). → guard срабатывал на смену источника при правке только лимита.
Где живёт: оба компонента (84272c5c guard-wiring + f248e277 always-send) — на main (боевой). То есть баг живой на liderra.ru: клиент с поставщиковым проектом не может понизить дневной лимит/сменить регионы/дни — ловит ложное «нельзя сменить источник». (На моей ветке с флагом ВКЛ маскировалось early-return'ом, поэтому не всплыло раньше.)
Фикс: $sourceFieldsTouched теперь сравнивает ЗНАЧЕНИЕ (новый приватный sourceValueChanged()), а не присутствие ключа. RED→GREEN тест ProjectServiceGuardWiringTest::test_update_does_not_invoke_guard_when_signal_identifier_present_but_unchanged. Глазами: после фикса смена лимита на защищённом проекте → 200, applies_from корректный, уведомление создалось. Регрессия: guard-wiring+guard-unit 18/18, фича-тесты 44/44, ProjectController/Projects зелёные.
Решить с владельцем: это давний прод-баг — возможно стоит отдельным хотфиксом в main, не дожидаясь всей фичи слепка. НЕ закоммичено.
✅ Эпик 4 онлайн-заморозка — ПОДТВЕРЖДЕНО (функционально, dev)
- Прогнал вживую на dev (режим временно online, окно 20ч МСК): заморозка —
SyncSupplierProjectJob(1)положил строку вsupplier_deferred_sync(project_id=1) и вышел, поставщика НЕ дёргал (отправка только в 00:05). Досыл —FlushDeferredOnlineSyncJobподBus::fakeпрочитал таблицу (было 1), поставилSyncSupplierProjectJobв очередь, очистил строку (стало 0). Режим вернул вbatch, таблица пуста. Расписание 00:05 в routes/console.php — доверяю (тест есть).
🟡 Давний фронт-тест-долг (НЕ моя регрессия)
- 22 упавших vitest-теста в 10 файлах, которых я не касался: SettingsView, ErrorView, LegalDocView, KanbanCard, DealsView, ChangePasswordCard, AdminPricingTiersView, DealDetailDrawerApi, ProjectsView (BulkActionsBar), AdminSupplierIntegrationView.manual-queue. Диффом подтверждено — я менял только 4 фронт-файла (ProjectDetailsDrawer.vue, NewProjectDialog.vue, AdminSupplierIntegrationView.vue, projectsStore.ts), эти спеки на components, что я не трогал → их результат = как на main. Долг существовал до сессии.
manual-queue.spec.tsтест «clicking Отметить выполнено» — стейл после рефактора window.confirm→v-dialog (падает и на чистом HEAD).
🟢 Нормативка / документы (план §«Финальная проверка»)
- Закрыть OPEN-вопрос §9a (формат отчёта о заливке) в реестре открытых вопросов — решение принято: «экран в админке». Не отмечено.
- Синк нормативки (Pravila / PSR / Tooling / CLAUDE.md) под 2 новые таблицы + новый экран/джоб — не делал (требует
normative-syncагента, НЕ коммитит). - Дрейф счётчиков схемы в шапке schema.sql (таблицы/индексы) — честно помечен в v8.54/v8.55, но точная пересверка вынесена в отдельный canon-sync. CHANGELOG_schema.md header («тридцать записей v8.33→…») стал стейл — не перечисляет v8.54/v8.55.
⚙️ Перед боевым (порядок выката, не баги)
- Полную
Pest --parallel+ полныйcomposer stan(Larastan) + lychee/gitleaks — НЕ гонял целиком (гонял широко: 883 бэк + полный фронт vitest). - prod-deploy-validator перед выкатом liderra.ru — не запускал.
- Порядок: включить флаг
routing_match_by_snapshot→ сутки наблюдать (drift=0, лиды доезжают) → ТОЛЬКО потом показывать клиентам Эпик 3 (иначе обещание «лиды придут» враньё). Экран «Вечерняя заливка» (Эпик 5) — для сверки глазами при наблюдении.
🧹 Мелочи окружения
- На dev поставлен пароль
passwordюзеруinessa.samojlova@example.org(вход в браузер) — безвредно. - Много untracked PNG-скринов проверок в корне + app/ (epic3-, epic5-, epic6-, time-aware-) — мусор, можно убрать.
- Ничего не запушено — все 20 коммитов локально на
feat/source-edit-snapshot-routing.
🌙 Сессия 26.06 (ночь) — конвейер «до выката» (владелец: «гони всё до выката, боевой не трогай»)
Полная регрессия (последовательно, --parallel OOM'ит воркеры — окруженческий квирк): 1756 тестов, 1750 ✅, 4 skip, 2 падения — оба разобраны:
- SchemaDeltaTest — ждал 72 таблицы/127 индексов, стало 74/128 из-за моих 2 таблиц (v8.54/v8.55). ✅ ПОЧИНЕНО: тест приведён к v8.55 (74/128/44). NB: бегущий счётчик в ШАПКЕ schema.sql сам дрейфует (заявляет 79/124) — давний отдельный canon-sync, не предмет теста.
- AutoPauseFlowTest «2-е письмо после 65 мин» — НЕ моя регрессия (агент pest-parallel-debugger, доказано: путь авто-паузы/письма байт-в-байт как main; мои LeadRouter-правки спят под флагом ВЫКЛ). Корень — пре-существующий time-of-day квирк: фикстура
createRoutingSnapshotFromProject(tests/Pest.php) сеет слепок только на «сегодня», аLeadRouter::activeSnapshotDate()после 21:00 МСК берёт «завтра» → 0 кандидатов → паузы/письма нет. До 21:00 → 5/5 зелёных. Фикс на будущее (не сейчас, чужой долг): заморозить часы до 21:00 в beforeEach ИЛИ сеять слепок на сегодня+завтра.
Нормативка (агент normative-sync): правок в 4 управляющих документах НЕ нужно (фича не добавила инструментов/плагинов/правил; Pravila v1.44 / PSR v3.24 / Tooling v2.25 / CLAUDE v2.47 без изменений, cross-refs зелёные). Схема-доки (schema.sql header v8.55 + CHANGELOG_schema.md v8.54/v8.55) уже синхронны в ветке.
- Опционально (через claude-md-management, НЕ сделано): обновить §6 CLAUDE.md «последняя продуктовая фича» на эту фичу; снять устаревшую ремарку шапки про «синхронизацию квинтета на 2.47 — follow-up» (уже закрыт в PSR/Tooling 14.06).
- ⚠️ Агент поймал:
git fetchк GitHub → 403 (аккаунт suspended). Рабочий remote — gitea; перед пушем проверить доступность.
Готовность боевого (агент prod-deploy-validator): ВЕРДИКТ GO — 8/8 зелёных (config читаем www-data/квирк107 закрыт, бэкап 14ч, место 56%, очередь жива с --timeout=300, nginx ок, fail2ban active, 0 лишних миграций; миграции фичи на проде ожидаемо отсутствуют).
Порядок выката (из вердикта валидатора, на утро владельцу):
- Проверить доступ к remote (gitea),
git pushветки → merge в main. php artisan migrate— миграции2026_06_25_120000(supplier_deferred_sync) →2026_06_25_130000(supplier_sync_runs) по timestamp.- Проверить GRANT для
crm_supplier_workerна обе новые таблицы (схема заявляет blanket-грантON ALL TABLESв db/02_grants.sql — подтвердить после миграции). - Проверить, что в cron есть
schedule:run(для FlushDeferredOnlineSyncJob 00:05 МСК = 21:05 UTC). sudo -u www-data php artisan config:cache && route:cache && view:cache— под www-data (квирк 107).- Флаг
routing_match_by_snapshotостаётся ВЫКЛ при первом выкате; включать осознанно отдельным действием → сутки наблюдать (drift=0) → потом Эпик 3 клиентам.
- Smoke:
curl -w '%{http_code}' https://liderra.ru/;php artisan migrate:status | tail;tail laravel.log.
Коммит: подготовлен, НЕ сделан (под стеной нужен подписанный эскейп владельца через клик AskUserQuestion — пока спит, кликнуть некому). Текст в scratchpad. Файлы к стейджу (явные пути, без PNG/чужих преды): ProjectService.php, ProjectServiceGuardWiringTest.php, phpstan-baseline.neon, notifications.ts, AppTopbar.vue, SchemaDeltaTest.php, 2 findings-дока. НЕ трогать: CLAUDE.md/settings.json/ПИЛОТ.md/observer/dev-indices (чужие преды).
Ключевые инварианты (не сломать при продолжении)
- Матч
queryCandidates:INNER JOIN projects ON id=snap.project_id+snap.snapshot_date=сегодня→ изменён источник (проект жив) = лид доезжает; удалён проект = лид не падает в сироту (JOIN отсекает), без краша. - Удаление проекта гейтится
assertCanMutateSource('delete')— нельзя, пока летит хвост (надо пауза + дождаться grace). Под флагом ВКЛ change_source разрешён, delete остаётся жёстким. - Даты: лимит/регион/дни → правило 18:00 (
firstLeadDate: до 18:00 завтра, после послезавтра); источник/удаление → правило 21:00 (computeGraceUntil). Согласованы: старый источник до N, новые настройки с N+1. - Тексты правил — единый источник
ProjectRuleMessages(бэкенд), фронт тянет через API (source_change_message), не дублирует.
🔧 26.06 — разбор «11 упавших» прод-прогона: НАЙДЕН реальный баг изоляции тестов + ошибка рецепта
Запрос владельца: «правь 11 оставшихся!» (после прод-прогона 1730/11).
Что сделал (systematic-debugging, безопасно — только локально, прод не трогал):
- Локально на ПРАВИЛЬНО собранной БД (
liderra_testing, собрана черезphp artisan migrate) все 8 «подозреваемых» файлов → зелёные (47 тестов, 0 падений). Значит это НЕ баги продукта/тестов. - Воспроизвёл условие прод-прогона локально: собрал БД
liderra_oneshotтем же «коротким» способом, что и на проде (psql -f db/schema.sql), и прогнал ВЕСЬ набор → 1758: 1755 прошло, 2 упало, 1 skip. - Оба падения идентичны:
register→ 422 «Аккаунт с таким email уже существует» (AuthFlowIntegrationTest,AuthLogCoverageTest) — ровно как на проде. - Контрольный прогон ВСЕГО набора на СВЕЖЕЙ
migrate-собранной БД (liderra_migrated) → 1758: 1757 прошло, 0 упало, 1 skip. На правильной свежей БД всё зелёное.
Корневая причина (РЕАЛЬНЫЙ баг, доказан RED→GREEN):
RegistrationService пишет users/tenants/email_verifications через BYPASSRLS-подключение pgsql_supplier (публичные роуты не выставляют app.current_tenant_id). Тесты AuthFlowIntegrationTest и AuthLogCoverageTest использовали DatabaseTransactions БЕЗ SharesSupplierPdo → записи через supplier-коннект коммитились мимо откатываемой транзакции и НЕ откатывались → тест переставал быть идемпотентным. На свежей БД — проходит; на «грязной»/повторном прогоне → 422 «email уже существует».
- RED: прогнал тест 1 раз → прошёл, но юзер
reg-log-test@example.ru(is_active=t) ОСТАЛСЯ в БД (утёк). Прогнал 2-й раз → 422 (тот же баг, что на проде). - Все ОСТАЛЬНЫЕ register-тесты (
RegistrationTest,ConfirmSetsEmailVerifiedAtTest,NewLeadEmailDefaultOnTest) уже используютSharesSupplierPdo— эти 2 были единственными выпавшими из конвенции.
Фикс: добавил uses(..., SharesSupplierPdo::class) в оба файла (app/tests/Feature/Auth/AuthFlowIntegrationTest.php, AuthLogCoverageTest.php).
- GREEN: оба файла полностью (16/16) проходят ДВАЖДЫ подряд на «грязной» БД, 0 утёкших юзеров.
Вторая причина части прод-падений — ОШИБКА РЕЦЕПТА прогона (не баг кода): throwaway-БД собиралась psql -f schema.sql вместо php artisan migrate. Тесты на RefreshDatabase (DemoSeeder и ещё ~6 файлов) зовут migrate:fresh, который ломается на psql-собранной БД (нет строк в migrations, расхождение round-trip). Для throwaway-БД под superuser (test_runner) надо собирать через php artisan migrate — как CI/dev. Рецепт исправлен в памяти feedback-prod-full-test-isolated-db.
Итог: «11» = не 11 багов. Реальный — один (баг изоляции 2-х auth-тестов, исправлен). Остальное — артефакты способа сборки тестовой БД + переиспользование грязной БД (pollution). На правильной свежей БД весь набор зелёный.