Files
portal/docs/superpowers/findings/2026-06-25-source-edit-session-tails.md
T
Дмитрий b33f23ee89 fix/tests: idempotency 2 auth-тестов — SharesSupplierPdo против утечки регистрации мимо отката
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>
2026-06-26 08:43:30 +03:00

25 KiB
Raw Blame History

Хвосты сессии — разблокировка смены источника (Эпики 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-portal SupplierPortalClientReport/RtProject/SessionRefreshCommand/FormProjectChannel. NB: удалять можно только ФАЙЛЫ внутри tests/Browser/, не саму папку (phpunit.xml testsuite → «directory not found»).

Рецепт безопасного полного прогона на проде (изолированно) — память feedback-prod-full-test-isolated-db:

  1. DROP DATABASE IF EXISTS liderra_testing WITH (FORCE) (PG16 — сам закрывает висящие коннекты) + CREATE ROLE test_runner LOGIN SUPERUSER PASSWORD ... + CREATE DATABASE liderra_testing OWNER test_runner.
  2. psql -d liderra_testing -f db/schema.sql (несёт схему + сиды suppliers/system_settings) + php artisan partitions:create-months --ahead=2 (иначе аудит-INSERT падает «нет партиции» → 422).
  3. Клон 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.
  4. Прогон detached (nohup ... &) — переживает обрыв ssh (квирк #109). НЕ set -e вокруг php artisan test (вернёт не-ноль на падениях → оборвёт до печати итога).
  5. Уборка: DROP базы+роли, rm клона/.env/логов. 🚨 Грабли: широкий pkill -f artisan на проде УБИВАЕТ боевой queue:work (systemd поднимает) — бить только по "artisan test" или /tmp/liderra-verify, не по artisan.

Дата: 2526.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)

  1. Баннер ДО/ПОСЛЕ 18:00 ПОДТВЕРЖДЕНО. 13:00 МСК → «вступят в силу с 26 июня» (завтра); 19:00 МСК → «27 июня» (послезавтра). Реальный отрендеренный баннер в дроуэре. Скрин verify-banner-before-18-26june.png.
  2. Колокольчик (Эпик 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)

  1. Прогнал вживую на 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 — доверяю (тест есть).

🟡 Давний фронт-тест-долг (НЕ моя регрессия)

  1. 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. Долг существовал до сессии.
  2. manual-queue.spec.ts тест «clicking Отметить выполнено» — стейл после рефактора window.confirm→v-dialog (падает и на чистом HEAD).

🟢 Нормативка / документы (план §«Финальная проверка»)

  1. Закрыть OPEN-вопрос §9a (формат отчёта о заливке) в реестре открытых вопросов — решение принято: «экран в админке». Не отмечено.
  2. Синк нормативки (Pravila / PSR / Tooling / CLAUDE.md) под 2 новые таблицы + новый экран/джоб — не делал (требует normative-sync агента, НЕ коммитит).
  3. Дрейф счётчиков схемы в шапке schema.sql (таблицы/индексы) — честно помечен в v8.54/v8.55, но точная пересверка вынесена в отдельный canon-sync. CHANGELOG_schema.md header («тридцать записей v8.33→…») стал стейл — не перечисляет v8.54/v8.55.

⚙️ Перед боевым (порядок выката, не баги)

  1. Полную Pest --parallel + полный composer stan (Larastan) + lychee/gitleaks — НЕ гонял целиком (гонял широко: 883 бэк + полный фронт vitest).
  2. prod-deploy-validator перед выкатом liderra.ru — не запускал.
  3. Порядок: включить флаг routing_match_by_snapshot → сутки наблюдать (drift=0, лиды доезжают) → ТОЛЬКО потом показывать клиентам Эпик 3 (иначе обещание «лиды придут» враньё). Экран «Вечерняя заливка» (Эпик 5) — для сверки глазами при наблюдении.

🧹 Мелочи окружения

  1. На dev поставлен пароль password юзеру inessa.samojlova@example.org (вход в браузер) — безвредно.
  2. Много untracked PNG-скринов проверок в корне + app/ (epic3-, epic5-, epic6-, time-aware-) — мусор, можно убрать.
  3. Ничего не запушено — все 20 коммитов локально на feat/source-edit-snapshot-routing.

🌙 Сессия 26.06 (ночь) — конвейер «до выката» (владелец: «гони всё до выката, боевой не трогай»)

Полная регрессия (последовательно, --parallel OOM'ит воркеры — окруженческий квирк): 1756 тестов, 1750 , 4 skip, 2 падения — оба разобраны:

  1. SchemaDeltaTest — ждал 72 таблицы/127 индексов, стало 74/128 из-за моих 2 таблиц (v8.54/v8.55). ПОЧИНЕНО: тест приведён к v8.55 (74/128/44). NB: бегущий счётчик в ШАПКЕ schema.sql сам дрейфует (заявляет 79/124) — давний отдельный canon-sync, не предмет теста.
  2. 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 лишних миграций; миграции фичи на проде ожидаемо отсутствуют).

Порядок выката (из вердикта валидатора, на утро владельцу):

  1. Проверить доступ к remote (gitea), git push ветки → merge в main.
  2. php artisan migrate — миграции 2026_06_25_120000 (supplier_deferred_sync) → 2026_06_25_130000 (supplier_sync_runs) по timestamp.
  3. Проверить GRANT для crm_supplier_worker на обе новые таблицы (схема заявляет blanket-грант ON ALL TABLES в db/02_grants.sql — подтвердить после миграции).
  4. Проверить, что в cron есть schedule:run (для FlushDeferredOnlineSyncJob 00:05 МСК = 21:05 UTC).
  5. sudo -u www-data php artisan config:cache && route:cache && view:cache — под www-data (квирк 107).
  6. Флаг 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, безопасно — только локально, прод не трогал):

  1. Локально на ПРАВИЛЬНО собранной БД (liderra_testing, собрана через php artisan migrate) все 8 «подозреваемых» файлов → зелёные (47 тестов, 0 падений). Значит это НЕ баги продукта/тестов.
  2. Воспроизвёл условие прод-прогона локально: собрал БД liderra_oneshot тем же «коротким» способом, что и на проде (psql -f db/schema.sql), и прогнал ВЕСЬ набор → 1758: 1755 прошло, 2 упало, 1 skip.
  3. Оба падения идентичны: register → 422 «Аккаунт с таким email уже существует» (AuthFlowIntegrationTest, AuthLogCoverageTest) — ровно как на проде.
  4. Контрольный прогон ВСЕГО набора на СВЕЖЕЙ 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). На правильной свежей БД весь набор зелёный.