From 51ad03ff659cc50192f8fb448e2ea472ec41f6c1 Mon Sep 17 00:00:00 2001 From: grok Date: Sat, 26 Sep 2026 15:28:10 +0700 Subject: [PATCH] =?UTF-8?q?test(e2e):=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=D0=B5=D1=82=D0=BE=D0=BA=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=82=D0=BA=D0=B8=20=D1=81=20?= =?UTF-8?q?=D0=B4=D0=B2=D0=BE=D0=B9=D0=BD=D1=8B=D0=BC=D0=B8=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: TASK-GROK-2 §4 — исправления принимаются только по прогону стенда, «на слово ничего не принимать»; исполнителям нужны ID владельца и контракты для новых сценариев K8/K9. Что: - report-evidence/acceptance-*.txt: для antigravity/fix-reject-device-shifts (795bba0), codex/fix-format-baseline (fd1ec3d), codex/fix-live-database (ea68e12) — команда, итог vitest, таблица «ID — было — стало — вывод» против основной ветки f5ac179 с тем же стендом (worktree, без коммитов в их ветки). Итог: fix-live-database — K3/N2, K4/N1 зелёные, открылись K5 и K6 по существу; обе ветки Codex — SYNC-A/B зелёные, потому что «style»- коммит fd1ec3d содержит исправление Antigravity (после prettier 0 строк diff с её веткой) — записано как находка для ревью. - NOTES: таблица дефектов с двойными ID, строки K6–K9, S9, A4, N12-DATE, контракты C4 (verify подписанной ведомости) и C5 (params пресета), CI (windows, артефакт evidence), раздел приёмки. Ни одна ветка не влита в основную, поэтому known-defects.json не сокращён. --- tests/e2e/NOTES-FOR-EXECUTORS.md | 106 +++++++++++++----- ...e-antigravity-fix-reject-device-shifts.txt | 16 +++ .../acceptance-codex-fix-format-baseline.txt | 31 +++++ .../acceptance-codex-fix-live-database.txt | 30 +++++ 4 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 tests/e2e/report-evidence/acceptance-antigravity-fix-reject-device-shifts.txt create mode 100644 tests/e2e/report-evidence/acceptance-codex-fix-format-baseline.txt create mode 100644 tests/e2e/report-evidence/acceptance-codex-fix-live-database.txt diff --git a/tests/e2e/NOTES-FOR-EXECUTORS.md b/tests/e2e/NOTES-FOR-EXECUTORS.md index 321099e..58820a2 100644 --- a/tests/e2e/NOTES-FOR-EXECUTORS.md +++ b/tests/e2e/NOTES-FOR-EXECUTORS.md @@ -66,40 +66,65 @@ API пока нет, поэтому сценарии `tests/e2e/30-owner-decisio сервер хранит только факт согласия. Тот же флаг стоит отдать в `/sync/pull` (employee payload). Сейчас все OWN красные с `404 Route ... not found` (C1–C3 не реализованы); -OWN-03..09 дополнительно упрутся в N2 (подтверждение смены падает 500). +OWN-03..09 дополнительно упрутся в K3/N2 (подтверждение смены падает 500). + +ID в названиях тестов — двойные «владелец/мой» (`[K3/N2]`, `[D6/OWN-01]`, `[K17/OWN-10]`, +`[K19/OWN-07]`), по таблице TASK-GROK-2 §2 и AUDIT.md. + +### C4. Проверочный пересчёт подписанной ведомости (K8/D1) + +- `GET /payroll/runs/:id/verify` (право `staff:payroll:read`) → + `200 { "run_id", "stored_total_minor", "recalculated_total_minor", "matches": boolean }`. +- Пересчёт идёт по снимку версий правил, параметров и ставок, действовавших при подписи + (и ревизиям смен), а не по текущим значениям. После смены ставки `matches` остаётся `true`. + +### C5. Параметры пресета в назначении оплаты (K9/D3) + +- `PUT /employees/:id/pay-assignment` принимает `params: { : integer }`, ключи — из + `param_declarations` выбранного правила (например, `shift_rate_minor`, `percent_bp`, + `upsell_rate_minor`, `avg_check_target_minor`, `avg_check_bonus_minor` у `waiter_upsell`); + неизвестный ключ → 400. `GET .../pay-assignment` возвращает их в `params`. +- Старые поля `rate_minor`/`percent_bp`/`guarantee_minor` можно оставить как сокращения. ## 2. Дефекты продукта, найденные сценариями (все красные сейчас) Команда и фактический вывод — `tests/e2e/evidence/e2e-run.txt`; таблица — `tests/e2e/REPORT.md`. -| ID | Файл | Что исправить | -| -------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| N1 | `src/portal.ts` POST /employees | `42P08 inconsistent types deduced for parameter $2`: явные `$2::varchar(26)`, `$5::text` или отдельный запрос профессии | -| N2 | `src/portal.ts` PATCH /work-sessions, `src/domain/payroll/service.ts` calculate/approve | `staff_outbox.occurred_at` NULL (23502): передавать `occurred_at` или `DEFAULT now()` новой миграцией | -| N4 | `service.ts` | после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен) | -| N3 | push-подписки | allowlist push-сервисов / запрет приватных адресов, `redirect:'manual'`; сейчас сервер подключается к `127.0.0.1` | -| N9 | `push-dispatcher.ts` | `ON CONFLICT ... DO UPDATE SET employee_id` → 409 для чужого endpoint | -| N10 | GET /professions, /org-units | требовать право справочника; терминал сейчас получает 200 | -| N11 | импорт | валидировать `kind` и схему `items` → 400 вместо 500 (23514) | -| N12 | PUT pay-assignment | `additionalProperties:false`, верхние границы сумм | -| F2 | `config.ts`/`identity.ts` | standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions) | -| F3 | POST/DELETE /session | в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию | -| F4 | вход | rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям) | -| F7 | POST /attendance/events | использовать `Idempotency-Key` / `staff_idempotency_keys` | -| F10 | `assertSameOrigin` | мутации по cookie без `Origin` отклонять | -| SYNC-A/B | `src/domain/sync/service.ts` | устройство не должно выставлять `confirmed`/`approved` (сейчас `applied`) | -| SYNC-C0 | `pull()` bootstrap | `SELECT ... status FROM staff_org_units` — такого столбца нет → 500 на любой начальной загрузке | -| SYNC-C | `pull()` bootstrap | при `has_more=true` отдаётся `next_cursor=latest`, остаток после 100 сущностей теряется; нужен курсор снимка | -| SYNC-D | портал/импорт | изменения вне sync не пишутся в `staff_sync_change_log` → терминал не узнаёт о новых/изменённых сотрудниках | -| SYNC-E | `executeCommand` | проверять `status='active'` сотрудника | -| SYNC-F | `executeCommand` | сохранять `org_unit_id` в `staff_attendance_events`; payload не может переопределить точку устройства | -| SYNC-G | `push()` | ошибки БД одной команды → `rejected` для неё, а не 500 и откат всего пакета | -| SYNC-H | `/sync/devices` | `assertCookieMutationOrigin` как в портале | -| SYNC-I | `requireManagerContext` | без токенов не выдавать права (как F2) | -| SYNC-J | `executeCommand` | окно времени отметки как в POST /attendance/events (−180 дней … +5 мин) | -| SYNC-K | `acknowledge()` | отклонять курсор больше `MAX(cursor_seq)` | +| ID | Файл | Что исправить | +| ---------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| K4/N1 | `src/portal.ts` POST /employees | `42P08 inconsistent types deduced for parameter $2`: явные `$2::varchar(26)`, `$5::text` или отдельный запрос профессии | +| K3/N2 | `src/portal.ts` PATCH /work-sessions, `src/domain/payroll/service.ts` calculate/approve | `staff_outbox.occurred_at` NULL (23502): передавать `occurred_at` или `DEFAULT now()` новой миграцией | +| K5/N4 | `service.ts` | после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен) | +| K12/N3 | push-подписки | allowlist push-сервисов / запрет приватных адресов, `redirect:'manual'`; сейчас сервер подключается к `127.0.0.1` | +| N9 | `push-dispatcher.ts` | `ON CONFLICT ... DO UPDATE SET employee_id` → 409 для чужого endpoint | +| N10 | GET /professions, /org-units | требовать право справочника; терминал сейчас получает 200 | +| N11 | импорт | валидировать `kind` и схему `items` → 400 вместо 500 (23514) | +| N12 | PUT pay-assignment | `additionalProperties:false`, верхние границы сумм | +| N12-DATE | `POST /payroll/calculate` | `month=2026-13` → 500 `22008`; проверять месяц 01–12 → 400 (диапазон `2026-02-30` уже даёт 400) | +| K6/D5 | `portal.ts` PATCH /work-sessions | смена подписанной ведомости: `409`, либо сторно в ledger; сейчас (на fix-live-database) 200 и начисление остаётся | +| K7/D2 | `operations.ts` импорт `payroll_entries` | дельта по сумме всех записей дня, вставка по ключу: две точки за день → 222,00 вместо 333,00 ₽; считать дельту по ключу | +| K8/D1 | расчёт/подпись | снимок правил и ставок в подписанной ведомости; контракт проверки C4 (ниже) | +| K9/D3 | PUT pay-assignment | сохранять `params` по `param_declarations` правила (C5 ниже); сейчас `shift_rate_minor` официанта теряется | +| S9 | миграция 0011 FK устройств | `ON DELETE SET NULL` на составном `(tenant_id, org_unit_id)` обнуляет `tenant_id` → 23502; `SET NULL (org_unit_id)` или RESTRICT | +| A4 | обработчик ошибок | в теле 5xx нет SQLSTATE (`code`) и текста PostgreSQL; подробности только в лог | +| K1/F2 | `config.ts`/`identity.ts` | standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions) | +| K10/F3 | POST/DELETE /session | в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию | +| K11/F4 | вход | rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям) | +| F7 | POST /attendance/events | использовать `Idempotency-Key` / `staff_idempotency_keys` | +| F10 | `assertSameOrigin` | мутации по cookie без `Origin` отклонять | +| K13/SYNC-A, K13/SYNC-B | `src/domain/sync/service.ts` | устройство не должно выставлять `confirmed`/`approved` (сейчас `applied`) | +| SYNC-C0 | `pull()` bootstrap | `SELECT ... status FROM staff_org_units` — такого столбца нет → 500 на любой начальной загрузке | +| S5/SYNC-C | `pull()` bootstrap | при `has_more=true` отдаётся `next_cursor=latest`, остаток после 100 сущностей теряется; нужен курсор снимка | +| S5/SYNC-D | портал/импорт | изменения вне sync не пишутся в `staff_sync_change_log` → терминал не узнаёт о новых/изменённых сотрудниках | +| S4/SYNC-E | `executeCommand` | проверять `status='active'` сотрудника | +| S4/SYNC-F | `executeCommand` | сохранять `org_unit_id` в `staff_attendance_events`; payload не может переопределить точку устройства | +| S3/SYNC-G | `push()` | ошибки БД одной команды → `rejected` для неё, а не 500 и откат всего пакета | +| S8/SYNC-H | `/sync/devices` | `assertCookieMutationOrigin` как в портале | +| K1/SYNC-I | `requireManagerContext` | без токенов не выдавать права (как F2) | +| S2/SYNC-J | `executeCommand` | окно времени отметки как в POST /attendance/events (−180 дней … +5 мин) | +| SYNC-K | `acknowledge()` | отклонять курсор больше `MAX(cursor_seq)` | -## 3. deploy/ (PILOT-1, PILOT-2) — Codex +## 3. deploy/ (K1/PILOT-1, K2/PILOT-2) — Codex - PILOT-1: все подстановки в `deploy/pilot/compose.yaml` → `${VAR:?VAR is required}` (сейчас 14 без `:?`: пустой `STANDALONE_MANAGER_TOKEN` молча превращает pilot в F2). @@ -118,6 +143,11 @@ OWN-03..09 дополнительно упрутся в N2 (подтвержде - Задаче `docker-build` нужен раннер с меткой `docker-host` (Docker daemon). Без него задача ждёт в очереди; остальные задачи не блокируются. - `format` красная, пока не исправлен F14 (23 файла на замер 2026-09-26, в основном `src/`, `docs/`, `tests/unit/`). +- `format` станет зелёной после влития `codex/fix-format-baseline` (проверено на ветке). +- Задача `windows` выключена, пока нет раннера с меткой `windows`: включается переменной + репозитория `KRONA_WINDOWS_RUNNER=true`; для полного e2e нужен секрет + `WINDOWS_E2E_PG_ADMIN_URL` (локальная PostgreSQL 17 на раннере). +- `tests/e2e/evidence/` не хранится в git; в CI это артефакт `e2e-evidence`. - Реальный Forgejo-раннер мной не запускался (нет API-токена) — не проверено. ## 5. N13 (гонка первого расчёта) и утечка текста ошибок БД @@ -139,3 +169,23 @@ CONFLICT DO NOTHING` со своим `runId` → строки/UPDATE по нес - Ответы 500 содержат сырой текст ошибки PostgreSQL (например `null value in column "occurred_at" of relation "staff_outbox" ...`) — клиенту видна схема БД. Для 5xx отдавать нейтральное сообщение, подробности — только в лог. + +## 6. Приёмка веток исполнителей (2026-09-26) + +Подробно — `tests/e2e/REPORT.md` и `tests/e2e/report-evidence/acceptance-*.txt`. + +- `codex/fix-live-database` (ea68e12): K3/N2 и K4/N1 зелёные на живой PostgreSQL 17. + Дальше по цепочке открылись K5/N4 (196800 вместо 98400 — двойное начисление) и + K6/D5 (отклонение оплаченной смены: 200, начисление осталось). A4 частично: `POST /employees` + больше не 500, но `month=2026-13` по-прежнему отдаёт `code=22008` с текстом. +- `codex/fix-format-baseline` (fd1ec3d): `prettier --check .` чистый. **Но коммит «style» + содержит функциональное исправление Antigravity**: `src/domain/sync/service.ts` и + `tests/unit/sync.test.ts` после нормализации prettier совпадают с + `antigravity/fix-reject-device-shifts` (0 строк diff) и отличаются от 331e490 на 255/139 + строк. Изменение поведения стоит вынести в отдельный коммит с объяснением или влить + ветку Antigravity отдельно, иначе история смешивает темы (AGENTS.md). +- `antigravity/fix-reject-device-shifts` (795bba0): K13/SYNC-A и K13/SYNC-B зелёные, + остальное без изменений. Ветка основана на 778ba2d, а не на 331e490. +- Ни одна ветка не влита в основную (f5ac179), поэтому ID из `known-defects.json` не удалены. +- Суммы в тестах кратны рублю: расчёт округляет строку до `rounding_unit_minor` + (по умолчанию 100 коп.). Если округление должно быть другим — это решение продукта, не теста. diff --git a/tests/e2e/report-evidence/acceptance-antigravity-fix-reject-device-shifts.txt b/tests/e2e/report-evidence/acceptance-antigravity-fix-reject-device-shifts.txt new file mode 100644 index 0000000..14ba662 --- /dev/null +++ b/tests/e2e/report-evidence/acceptance-antigravity-fix-reject-device-shifts.txt @@ -0,0 +1,16 @@ +# Приёмка ветки antigravity/fix-reject-device-shifts @ 795bba0ce8eb2621a395c6bd794d075609c1a3eb (2026-09-26, UTC+7) +# Код продукта — ветка исполнителя (git worktree --detach origin/antigravity/fix-reject-device-shifts), поверх — tests/e2e и scripts/test из grok/e2e-harness @ 3475133 (без коммитов в ветку исполнителя). +# PostgreSQL 17.11, мигратор NOSUPERUSER, сервер под krona_staff_app. + +$ cd && E2E_MIGRATIONS=roundtrip E2E_DATABASE_NAME=krona_staff_acc node scripts/test/run-e2e.mjs +== roundtrip OK: rolledBack=11 reapplied=11 + Tests 43 failed | 11 passed (54) +| K13/SYNC-A | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +| K13/SYNC-B | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +exit=1 + +# Было (основная ветка f5ac179 + тот же стенд): Tests 45 failed | 9 passed (54) ; run-e2e OK +# Отличия по ID (compare по e2e-results.json, ULID/порты замаскированы): +| ID | было | стало | фактический вывод | +| K13/SYNC-A | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-","command_type":"upsert_work_session","status":"applied","result":{"id":"","revision":1,"status" / стало: passed | +| K13/SYNC-B | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-","command_type":"upsert_timesheet_entry","status":"applied","result":{"id":"","revision":1,"stat / стало: passed | diff --git a/tests/e2e/report-evidence/acceptance-codex-fix-format-baseline.txt b/tests/e2e/report-evidence/acceptance-codex-fix-format-baseline.txt new file mode 100644 index 0000000..ee05cee --- /dev/null +++ b/tests/e2e/report-evidence/acceptance-codex-fix-format-baseline.txt @@ -0,0 +1,31 @@ +# Приёмка ветки codex/fix-format-baseline @ fd1ec3d8197c21afe15a8dc8101816c52226ebef (2026-09-26, UTC+7) +# Код продукта — ветка исполнителя (git worktree --detach origin/codex/fix-format-baseline), поверх — tests/e2e и scripts/test из grok/e2e-harness @ 3475133 (без коммитов в ветку исполнителя). +# PostgreSQL 17.11, мигратор NOSUPERUSER, сервер под krona_staff_app. + +$ cd && E2E_MIGRATIONS=roundtrip E2E_DATABASE_NAME=krona_staff_acc node scripts/test/run-e2e.mjs +== roundtrip OK: rolledBack=11 reapplied=11 + Tests 43 failed | 11 passed (54) +| K13/SYNC-A | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +| K13/SYNC-B | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +exit=1 + +# Было (основная ветка f5ac179 + тот же стенд): Tests 45 failed | 9 passed (54) ; run-e2e OK +# Отличия по ID (compare по e2e-results.json, ULID/порты замаскированы): +| ID | было | стало | фактический вывод | +| K13/SYNC-A | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-","command_type":"upsert_work_session","status":"applied","result":{"id":"","revision":1,"status" / стало: passed | +| K13/SYNC-B | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-","command_type":"upsert_timesheet_entry","status":"applied","result":{"id":"","revision":1,"stat / стало: passed | + +# F14: форматирование ветки (оверлей tests/e2e/evidence удалён перед проверкой — в ветке исполнителя нет записи .gitignore для него) +$ node node_modules/prettier/bin/prettier.cjs --check . +Checking formatting... +All matched files use Prettier code style! +# Было (основная ветка f5ac179): [warn] Code style issues found in 23 files. + +# Находка: коммит fd1ec3d «style: establish repository formatting baseline» содержит не только форматирование. +# Сравнение после нормализации prettier (--config .prettierrc.json), число строк diff: +# src/domain/sync/service.ts 331e490 → fd1ec3d: 255; antigravity/fix-reject-device-shifts → fd1ec3d: 0 +# tests/unit/sync.test.ts 331e490 → fd1ec3d: 139; antigravity/fix-reject-device-shifts → fd1ec3d: 0 +# src/sync.ts, src/domain/sync/cursor.ts: antigravity → fd1ec3d: 0 +# То есть в «style»-коммит вошло функциональное исправление Antigravity (отклонение +# upsert_work_session/upsert_timesheet_entry, COMMAND_NOT_SUPPORTED) — поэтому K13/SYNC-A/B +# зелёные и на этой ветке. Для ревью это смешение тем: изменение поведения скрыто в форматировании. diff --git a/tests/e2e/report-evidence/acceptance-codex-fix-live-database.txt b/tests/e2e/report-evidence/acceptance-codex-fix-live-database.txt new file mode 100644 index 0000000..9673dfa --- /dev/null +++ b/tests/e2e/report-evidence/acceptance-codex-fix-live-database.txt @@ -0,0 +1,30 @@ +# Приёмка ветки codex/fix-live-database @ ea68e123506cc29276514f67b1dda7cef77057df (2026-09-26, UTC+7) +# Код продукта — ветка исполнителя (git worktree --detach origin/codex/fix-live-database), поверх — tests/e2e и scripts/test из grok/e2e-harness @ 3475133 (без коммитов в ветку исполнителя). +# PostgreSQL 17.11, мигратор NOSUPERUSER, сервер под krona_staff_app. + +$ cd && E2E_MIGRATIONS=roundtrip E2E_DATABASE_NAME=krona_staff_acc node scripts/test/run-e2e.mjs +== roundtrip OK: rolledBack=11 reapplied=11 + Tests 41 failed | 13 passed (54) +| K13/SYNC-A | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +| K13/SYNC-B | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +| K3/N2 | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +| K4/N1 | GREEN | 1/1 | зелёный — ИСПРАВЛЕН, уберите из known-defects.json | +exit=1 + +# Было (основная ветка f5ac179 + тот же стенд): Tests 45 failed | 9 passed (54) ; run-e2e OK +# Отличия по ID (compare по e2e-results.json, ULID/порты замаскированы): +| ID | было | стало | фактический вывод | +| A4 | RED | RED | было: AssertionError: тела 500 с деталями PostgreSQL: expected [ …(2) ] to deeply equal [] / стало: AssertionError: тела 500 с деталями PostgreSQL: expected [ { …(2) } ] to deeply equal [] | +| K13/SYNC-A | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-","command_type":"upsert_work_session","status":"applied","result":{"id":"","revision":1,"status" / стало: passed | +| K13/SYNC-B | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-","command_type":"upsert_timesheet_entry","status":"applied","result":{"id":"","revision":1,"stat / стало: passed | +| K3/N2 | RED | GREEN | было: AssertionError: подтверждение смены: HTTP 500 {"statusCode":500,"code":"23502","error":"Internal Server Error","message":"null value in column \"occurred_at\" of relation / стало: passed | +| K4/N1 | RED | GREEN | было: AssertionError: HTTP 500 {"statusCode":500,"code":"42P08","error":"Internal Server Error","message":"inconsistent types deduced for parameter $2"}: expected 500 to be 201 / стало: passed | +| K5/N4 | RED | RED | было: AssertionError: подтверждение смены (предусловие, см. N2): HTTP 500 {"statusCode":500,"code":"23502","error":"Internal Server Error","message":"null value in column \"occ / стало: AssertionError: смена должна быть начислена ровно один раз: expected 196800n to be 98400n // Object.is equality | +| K6/D5 | RED | RED | было: AssertionError: подтверждение смены (предусловие; падает, пока открыт K3): HTTP 500 {"statusCode":500,"code":"23502","error":"Internal Server Error","message":"null value / стало: AssertionError: после отклонения оплаченной смены должно быть сторно: итог начислений 0: expected 98400n to be 0n // Object.is equality | +| K8/D1 | RED | RED | было: AssertionError: подтверждение смены (предусловие; падает, пока открыт K3): HTTP 500 {"statusCode":500,"code":"23502","error":"Internal Server Error","message":"null value / стало: AssertionError: C4 verify: HTTP 404 {"message":"Route GET:/api/v1/staff/payroll/runs//verify not found","error":"Not Found","statusCode":404}: expected 404 to be 20 | + +# F14: форматирование ветки (оверлей tests/e2e/evidence удалён перед проверкой — в ветке исполнителя нет записи .gitignore для него) +$ node node_modules/prettier/bin/prettier.cjs --check . +Checking formatting... +All matched files use Prettier code style! +# Было (основная ветка f5ac179): [warn] Code style issues found in 23 files.