fake()->unique() builds a fresh UniqueGenerator per definition() call, so
uniqueness is not guaranteed within a batch — names collided on the
(tenant_id, name) UNIQUE under pest --parallel. Append Str::random(8)
(62^8 ≈ 2e14 space) to eliminate the collision.
Verified: ProjectBulkActions 15/15 ×2 parallel runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 4 live-smoke выявил: единственный .el-switch формы — include/exclude
регионов (regions_reverse), НЕ статус active/paused. Старый код кликал его
по dto.active → ошибочно ставил regions_reverse. Статус — дефолт портала
(active), UI-switch для него нет → switch-блок удалён.
recon-doc 2026-05-19-rt-project-form-locators.md: +секция Live-smoke
(domain-формат валидируется, multi-source save = N проектов, switch = regions,
type/tab re-render); row 6 исправлен.
Найдено при Task 4 live-smoke form-канала:
- type-select и вкладка «Список» кликались безусловно → re-click уже-активного
значения ремоунтит Element UI tab-pane (textarea детачится). Теперь кликаем
только при реальной смене значения + waitForTimeout после смены типа.
- defensive: проверка непустого textarea после fill content.
- diag: на status!=OK дамп фактически отправленного rt-project-save body в stderr.
- fillForm rewritten to label-for locators (.el-form-item:has([for="..."])) from recon 2026-05-19
- createOp: external_id from page.waitForResponse('rt-project-save') body, not DOM
- updateOp: same save endpoint intercept; row found by data-id or text
- listOp: Vuex state strategy 1, DOM scrape strategy 2, empty array fallback
- Known gaps (JSDoc + stderr warnings): workdays not in add-project form (portal default);
regions require id->name mapping (skipped in Tier-2 MVP, logged to stderr)
- Test: HTTP fixture server serves rt-form-element-ui.html + handles /admin/visit/rt-project-save
- Fixture: .v-dialog--active wrapper + 10 .el-form-item (label[for=...]) + type-select popup in body
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Логин-страница уже в состоянии networkidle → waitForLoadState резолвился
мгновенно (до пост-логин редиректа), скрипт хватал PHPSESSID
неаутентифицированной логин-страницы. CSV-сверка 11:00 (19.05) упала
"load-reports returned non-array response" — портал отдал HTTP 200
+ HTML логин-страницы вместо JSON-массива отчётов.
После клика submit:
- waitForFunction опрашивает исчезновение #loginform-username из DOM
(переживает навигацию);
- guard exit 1, если форма осталась — отклонённый логин больше не
маскируется под «успех» (exit 0).
Verified: 2× RefreshSupplierSessionJob → валидная сессия (load-reports
JSON-массив из 39 отчётов); CsvReconcileJob id=7 status=ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Финальное code-review вскрыло CRITICAL: dedup-сверка findOnPortal и
manualQueueResolve матчат строки портала по {platform, signal_type,
unique_key}, но listProjects отдавал сырое тело rt-projects-load.
Сырая форма (verified из recon-снапшота 2026-05-19):
- ответ — конверт {projects:[443 строки], tags, users, ...}, НЕ голый массив
→ listProjects возвращал весь dict, findOnPortal итерировал по ключам
конверта (projects/tags/...) вместо строк проектов;
- строка проекта: {id, name:"B<n>_<key>", type:"hosts|calls|sms", content}
— без platform/signal_type/unique_key.
Фикс:
- SupplierPortalClient::listProjects — извлекает body['projects'].
- AjaxProjectChannel::listProjects — нормализует сырые строки в контракт
SupplierProjectChannel: platform <- префикс name "B<n>_", signal_type <-
type (hosts->site/calls->call/sms->sms), unique_key <- content. Сырые
поля сохранены. findOnPortal + manualQueueResolve матчат корректно.
- AjaxProjectChannelTest — тест нормализации против фактической формы
портала (не идеального мока); SupplierPortalClientRtProjectTest —
listProjects против конверта {projects}.
Также (code-review I1): findOnPortal catch — Log::warning проглоченного
исключения, иначе провал дедупа невидим (молчаливый дубль rt-проекта).
Code-review I2 (partial-unique индекс supplier_manual_sync_queue от
дубль-эскалаций при job-retry) и I3 (lockForUpdate в manualQueueResolve) —
follow-up до прод-релиза (эпик гейтится Б-1, не в проде).
Регрессия Pest 973/970/0 / 3 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Таблица pending-записей яруса 3 + кнопка «Отметить выполнено» с confirm-
диалогом, дёргает POST .../manual-queue/{id}/resolve. Реюз существующего
админ-экрана интеграции с поставщиком (после «Истории сверок»).
NB: spec в tests/Frontend/ (vitest include — tests/Frontend/**, не
resources/.../__tests__/ как указал план Step 11.1). loadManualQueue
defensive Array.isArray-guard — иначе onMounted в чужих spec'ах
(mockResolvedValue без queue-ключа) ловил undefined.length.
Spec §4.6. Task 11 of 12. Vitest 5/5 (2 новых + 3 существующих).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET /api/admin/supplier-integration/manual-queue — pending список (limit 100).
POST /manual-queue/{id}/resolve — оператор пометил, что вручную создал проект
на портале; reconcile через channel->listProjects() по (platform, signal_type,
unique_key), 409 если не найден.
ОТКЛОНЕНИЕ ОТ plan Step 10.3: план писал portal external_id прямо в
projects.supplier_b*_project_id (FK на local supplier_projects.id) — FK
violation. Resolve делает firstOrCreate local supplier_projects row с
verified external_id, в FK пишет local id.
Routes — в группе saas-admin (web.php, EnsureSaasAdmin стаб). Task 10 of 12.
Tests 4/4 (index pending / exclude resolved / resolve match / resolve 409).
Spec §4.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Запас ~3 часа до портального дедлайна 21:00 — эскалация на ярус 2/3
(медленный браузер / ручной оператор) происходит в рабочее время.
RefreshSupplierSessionJob daily — на 15 мин раньше sync (17:45).
Hourly RefreshSupplierSessionJob — без изменений.
Spec §4.7. Task 9 of 12. Tests 2/2 (cron expression + timezone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Оба job'а инжектят SupplierProjectChannel (DI → FailoverProjectChannel)
вместо прямого SupplierPortalClient. Catch TierEscalatedException +
WindowDeferredException — эскалация/перенос пропускают элемент, не валят job.
SyncSupplierProjectJob (singular): handle переписан — find-or-create local
supplier_projects row, portal-create через channel. ОТКЛОНЕНИЕ ОТ plan Step 8.1:
план писал channel-результат (portal external_id) прямо в projects.supplier_b*_
project_id, но эта колонка — FK на supplier_projects.id (local), не portal id.
Сохранена семантика ensureSupplierProject — job создаёт local row с
supplier_external_id и пишет в FK local id. ensureSupplierProject удалён из
SupplierPortalClient (был единственный consumer — этот job).
SyncSupplierProjectsJob (plural): handle/syncOne принимают channel; create →
createProjectForLiderra, update → updateProjectForLiderra (context-project из
liderraProjects->first() для project_id в очереди яруса 3).
Tests: singular переписан под SupplierProjectChannel mock (6 tests, incl.
idempotency reuse); plural — handle(AjaxProjectChannel) для non-failover
ветки (Http::fake-контракт сохранён). Larastan отложен на T12 (worktree
quirk — гонится в основной копии). Регрессия Pest 966/963/0 / 3 skipped.
Spec §5. Task 8 of 12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
create/update/list через headless Chromium по образцу refresh-session.js.
Селекторы зафиксированы из recon-снапшота rt-add-project-form.yml (Task 1).
stdin/stdout JSON, exit codes 0/1/2/3/4 (success/auth/selector/timeout/input).
Фикстурный тест против локального HTML — без живого портала. Runner —
встроенный node:test (app/playwright не использует @playwright/test, только
playwright core); skipLogin режим открывает фикстуру напрямую.
Spec §4.3. Task 6 of 12. Node-тесты 2/2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
listProjects() матч по (platform, signal_type, unique_key) до create.
Защита от дубля при полу-успехе яруса 1 (create прошёл на портале, но
локальная запись не сохранилась → следующий запуск дублировал бы).
listProjects-сбой проглатывается — ярус-эскалация всё равно покроет.
Spec §4.4 шаг 2, §7. Task 5 of 12. Тесты 7/7 (19 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live discovery через Playwright MCP (Task 1):
- создан LIDPOTOK_TEST_DELETE_ME (B1+B2+B3) → 3 rt-проекта на портале;
- записаны сетевые запросы /admin/visit/rt-*;
- все три проекта удалены вручную, портал чист.
Endpoints (verified):
- POST /admin/visit/rt-project-save (create id:0, update id:N — same URL)
- POST /admin/visit/rt-project-delete (id строкой)
- GET /admin/visit/rt-projects-load?src=none
Все три — application/json. Конверт ответа:
- success: HTTP 200 + {status:OK, message, result, id?:string}
- error: HTTP 200 + {status:Error, message, result:null}
ID — строка (12721245), приводится к int (fits в int64).
Один save с B1+B2+B3 включёнными создаёт 3 rt-проекта — toPayload()
шлёт ровно один платформенный флаг (srcrt|srcbl|srcmt).
SupplierPortalClient:
- docblock переписан под verified контракт
- listProjects: путь /admin/visit/rt-projects-load + ?src=none query
- saveProject: путь /admin/visit/rt-project-save, asJson, парсинг id
- updateProject: тот же endpoint что save, id:N в body
- deleteProject: путь /admin/visit/rt-project-delete, asJson, id строкой
- new assertStatusOk() — HTTP 200 + status:Error → SupplierClientException
- toPayload(): полный Vuex-payload с маппингом DTO → portal:
- platform B1/B2/B3 → srcrt/srcbl/srcmt (single-true)
- signalType site/call/sms → type:hosts/calls/sms
- workdays int[] → string[]
- status active/paused → bool
- + tag:_lidpotok, name/content из uniqueKey, defaults для show/depth/etc
Tests:
- new: tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php (7 tests,
contract: save+update+delete+list + 2 status:Error error-paths + B2/calls
mapping)
- Sync/Cleanup/Unit тесты обновлены под новый URL + envelope shape.
Закрывает spec §1 honest-caveat «placeholder, не верифицирован»
и журнал решений запись 9. Регрессия: Pest 944/941/0 failed / 3 skipped
/ 2768 assertions / 59.2s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure regex/JSON, 0 LLM calls. 4 Vitest tests GREEN. Per ADR-011 + spec §6.1.
Smoke run surfaces REAL drift (DONE_WITH_CONCERNS — plan B5 said «that's
a real signal, document, don't fix here»): 9 plugins in
~/.claude/settings.json enabledPlugins NOT formalized by exact
«name@source» string in Tooling Прил. Н:
- frontend-design@claude-plugins-official (informally as #30
«Frontend Design plugin»)
- 8× ToB plugins @trailofbits (differential-review, audit-context-
building, supply-chain-risk-auditor, insecure-defaults, sharp-
edges, static-analysis, variant-analysis, agentic-actions-auditor)
informally as #39 «Trail of Bits Skills»
This is naming-vocabulary mismatch (Tooling uses human-readable
names; settings.json uses machine names). Not architectural drift.
Resolution options for follow-up:
- Add machine names as «external_id» attribute to Tooling Прил. Н rows.
- Add tools/.l1-watcher-aliases.txt with accepted machine→human map.
Until resolved: C1 will FAIL on lefthook (C5 wiring) — addressed in
C5 by adding alias mechanism OR temporarily downgrade to WARN.
Also fixed CLI guard bug in observer-stop-hook.mjs (B3) and l1-watcher
— old guard `import.meta.url === \`file://\${argv[1]}\`` did not match
on Windows (file:/// triple-slash vs file:// double-slash + relative
argv[1]). New guard: argv[1].endsWith('/<filename>.mjs').
Weekly GH Actions cron (Mon 09:00 MSK) opens issue on drift.
Vitest config extended to ../tools/*.test.mjs with exclude for ruflo-*
and subagent-prompt-prefix tests (pre-existing, not part of brain
governance).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Used by Stop-hook before JSONL write. 6 Vitest cases including
idempotence and recursive object sanitization. Per Pravila §16.2 +
ADR-011 + spec §5.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Финальный code-review эпика: insertGetId log-строки был вне try → при
падении самого insertGetId (БД недоступна) finally не освобождал
Cache::lock → lock висел LOCK_TTL_SECONDS (600с), пропуская 2 следующих
запуска. Перенесён внутрь try; $logId инициализируется null, catch
guard'ит обращение к нему.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>