test(e2e): приёмка веток исполнителей и заметки с двойными ID

Зачем: 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 не сокращён.
This commit is contained in:
grok 2026-09-26 15:28:10 +07:00
parent 3475133b10
commit 51ad03ff65
4 changed files with 155 additions and 28 deletions

View file

@ -66,40 +66,65 @@ API пока нет, поэтому сценарии `tests/e2e/30-owner-decisio
сервер хранит только факт согласия. Тот же флаг стоит отдать в `/sync/pull` (employee payload). сервер хранит только факт согласия. Тот же флаг стоит отдать в `/sync/pull` (employee payload).
Сейчас все OWN красные с `404 Route ... not found` (C1–C3 не реализованы); Сейчас все 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: { <key>: 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. Дефекты продукта, найденные сценариями (все красные сейчас) ## 2. Дефекты продукта, найденные сценариями (все красные сейчас)
Команда и фактический вывод — `tests/e2e/evidence/e2e-run.txt`; таблица — `tests/e2e/REPORT.md`. Команда и фактический вывод — `tests/e2e/evidence/e2e-run.txt`; таблица — `tests/e2e/REPORT.md`.
| ID | Файл | Что исправить | | ID | Файл | Что исправить |
| -------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | ---------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| N1 | `src/portal.ts` POST /employees | `42P08 inconsistent types deduced for parameter $2`: явные `$2::varchar(26)`, `$5::text` или отдельный запрос профессии | | K4/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()` новой миграцией | | 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()` новой миграцией |
| N4 | `service.ts` | после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен) | | K5/N4 | `service.ts` | после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен) |
| N3 | push-подписки | allowlist push-сервисов / запрет приватных адресов, `redirect:'manual'`; сейчас сервер подключается к `127.0.0.1` | | K12/N3 | push-подписки | allowlist push-сервисов / запрет приватных адресов, `redirect:'manual'`; сейчас сервер подключается к `127.0.0.1` |
| N9 | `push-dispatcher.ts` | `ON CONFLICT ... DO UPDATE SET employee_id` → 409 для чужого endpoint | | N9 | `push-dispatcher.ts` | `ON CONFLICT ... DO UPDATE SET employee_id` → 409 для чужого endpoint |
| N10 | GET /professions, /org-units | требовать право справочника; терминал сейчас получает 200 | | N10 | GET /professions, /org-units | требовать право справочника; терминал сейчас получает 200 |
| N11 | импорт | валидировать `kind` и схему `items` → 400 вместо 500 (23514) | | N11 | импорт | валидировать `kind` и схему `items` → 400 вместо 500 (23514) |
| N12 | PUT pay-assignment | `additionalProperties:false`, верхние границы сумм | | N12 | PUT pay-assignment | `additionalProperties:false`, верхние границы сумм |
| F2 | `config.ts`/`identity.ts` | standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions) | | N12-DATE | `POST /payroll/calculate` | `month=2026-13` → 500 `22008`; проверять месяц 01–12 → 400 (диапазон `2026-02-30` уже даёт 400) |
| F3 | POST/DELETE /session | в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию | | K6/D5 | `portal.ts` PATCH /work-sessions | смена подписанной ведомости: `409`, либо сторно в ledger; сейчас (на fix-live-database) 200 и начисление остаётся |
| F4 | вход | rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям) | | K7/D2 | `operations.ts` импорт `payroll_entries` | дельта по сумме всех записей дня, вставка по ключу: две точки за день → 222,00 вместо 333,00 ₽; считать дельту по ключу |
| F7 | POST /attendance/events | использовать `Idempotency-Key` / `staff_idempotency_keys` | | K8/D1 | расчёт/подпись | снимок правил и ставок в подписанной ведомости; контракт проверки C4 (ниже) |
| F10 | `assertSameOrigin` | мутации по cookie без `Origin` отклонять | | K9/D3 | PUT pay-assignment | сохранять `params` по `param_declarations` правила (C5 ниже); сейчас `shift_rate_minor` официанта теряется |
| SYNC-A/B | `src/domain/sync/service.ts` | устройство не должно выставлять `confirmed`/`approved` (сейчас `applied`) | | S9 | миграция 0011 FK устройств | `ON DELETE SET NULL` на составном `(tenant_id, org_unit_id)` обнуляет `tenant_id` → 23502; `SET NULL (org_unit_id)` или RESTRICT |
| SYNC-C0 | `pull()` bootstrap | `SELECT ... status FROM staff_org_units` — такого столбца нет → 500 на любой начальной загрузке | | A4 | обработчик ошибок | в теле 5xx нет SQLSTATE (`code`) и текста PostgreSQL; подробности только в лог |
| SYNC-C | `pull()` bootstrap | при `has_more=true` отдаётся `next_cursor=latest`, остаток после 100 сущностей теряется; нужен курсор снимка | | K1/F2 | `config.ts`/`identity.ts` | standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions) |
| SYNC-D | портал/импорт | изменения вне sync не пишутся в `staff_sync_change_log` → терминал не узнаёт о новых/изменённых сотрудниках | | K10/F3 | POST/DELETE /session | в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию |
| SYNC-E | `executeCommand` | проверять `status='active'` сотрудника | | K11/F4 | вход | rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям) |
| SYNC-F | `executeCommand` | сохранять `org_unit_id` в `staff_attendance_events`; payload не может переопределить точку устройства | | F7 | POST /attendance/events | использовать `Idempotency-Key` / `staff_idempotency_keys` |
| SYNC-G | `push()` | ошибки БД одной команды → `rejected` для неё, а не 500 и откат всего пакета | | F10 | `assertSameOrigin` | мутации по cookie без `Origin` отклонять |
| SYNC-H | `/sync/devices` | `assertCookieMutationOrigin` как в портале | | K13/SYNC-A, K13/SYNC-B | `src/domain/sync/service.ts` | устройство не должно выставлять `confirmed`/`approved` (сейчас `applied`) |
| SYNC-I | `requireManagerContext` | без токенов не выдавать права (как F2) | | SYNC-C0 | `pull()` bootstrap | `SELECT ... status FROM staff_org_units` — такого столбца нет → 500 на любой начальной загрузке |
| SYNC-J | `executeCommand` | окно времени отметки как в POST /attendance/events (−180 дней … +5 мин) | | S5/SYNC-C | `pull()` bootstrap | при `has_more=true` отдаётся `next_cursor=latest`, остаток после 100 сущностей теряется; нужен курсор снимка |
| SYNC-K | `acknowledge()` | отклонять курсор больше `MAX(cursor_seq)` | | 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}` - PILOT-1: все подстановки в `deploy/pilot/compose.yaml` → `${VAR:?VAR is required}`
(сейчас 14 без `:?`: пустой `STANDALONE_MANAGER_TOKEN` молча превращает pilot в F2). (сейчас 14 без `:?`: пустой `STANDALONE_MANAGER_TOKEN` молча превращает pilot в F2).
@ -118,6 +143,11 @@ OWN-03..09 дополнительно упрутся в N2 (подтвержде
- Задаче `docker-build` нужен раннер с меткой `docker-host` (Docker daemon). - Задаче `docker-build` нужен раннер с меткой `docker-host` (Docker daemon).
Без него задача ждёт в очереди; остальные задачи не блокируются. Без него задача ждёт в очереди; остальные задачи не блокируются.
- `format` красная, пока не исправлен F14 (23 файла на замер 2026-09-26, в основном `src/`, `docs/`, `tests/unit/`). - `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-токена) — не проверено. - Реальный Forgejo-раннер мной не запускался (нет API-токена) — не проверено.
## 5. N13 (гонка первого расчёта) и утечка текста ошибок БД ## 5. N13 (гонка первого расчёта) и утечка текста ошибок БД
@ -139,3 +169,23 @@ CONFLICT DO NOTHING` со своим `runId` → строки/UPDATE по нес
- Ответы 500 содержат сырой текст ошибки PostgreSQL (например - Ответы 500 содержат сырой текст ошибки PostgreSQL (например
`null value in column "occurred_at" of relation "staff_outbox" ...`) — клиенту видна `null value in column "occurred_at" of relation "staff_outbox" ...`) — клиенту видна
схема БД. Для 5xx отдавать нейтральное сообщение, подробности — только в лог. схема БД. Для 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 коп.). Если округление должно быть другим — это решение продукта, не теста.

View file

@ -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 <worktree> && 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-<ulid>","command_type":"upsert_work_session","status":"applied","result":{"id":"<ulid>","revision":1,"status" / стало: passed |
| K13/SYNC-B | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-<ulid>","command_type":"upsert_timesheet_entry","status":"applied","result":{"id":"<ulid>","revision":1,"stat / стало: passed |

View file

@ -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 <worktree> && 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-<ulid>","command_type":"upsert_work_session","status":"applied","result":{"id":"<ulid>","revision":1,"status" / стало: passed |
| K13/SYNC-B | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-<ulid>","command_type":"upsert_timesheet_entry","status":"applied","result":{"id":"<ulid>","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
# зелёные и на этой ветке. Для ревью это смешение тем: изменение поведения скрыто в форматировании.

View file

@ -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 <worktree> && 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-<ulid>","command_type":"upsert_work_session","status":"applied","result":{"id":"<ulid>","revision":1,"status" / стало: passed |
| K13/SYNC-B | RED | GREEN | было: AssertionError: HTTP 200 {"results":[{"idempotency_key":"e2e-<ulid>","command_type":"upsert_timesheet_entry","status":"applied","result":{"id":"<ulid>","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/<ulid>/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.