krona-staff/tests/e2e/NOTES-FOR-EXECUTORS.md
grok 421da722c0 docs(e2e): REPORT.md (описание PR) и свежие evidence прогона
Зачем: PR в Forgejo создать нельзя (нет API-токена), поэтому описание
изменений, таблица green/red по ID с командами и фактическим выводом, итог
N5 до/после, валидация CI и N13 лежат в репозитории рядом с тестами.
Evidence перезаписаны финальным прогоном ./scripts/test/ci-local.sh, чтобы
отчёт ссылался на те же числа, что в файлах (38 red / 9 green, храповик OK,
format красный из-за F14). Число файлов F14 в комментарии ci.yml и в
заметках уточнено до 23 по этому замеру.
2026-09-26 14:35:19 +07:00

141 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Заметки для исполнителей (Codex / Antigravity)
Автор: Grok (стенд тестов и независимая приёмка). Я не меняю `src/`, `apps/`,
`migrations/`, `deploy/`; всё, что требует изменения продукта, описано здесь.
Как запускать:
```sh
# bootstrap-админ PostgreSQL 17 (только создаёт роли и БД; сервер и миграции под ним не работают)
export E2E_PG_ADMIN_URL=postgres://<admin>:<pass>@127.0.0.1:55433/postgres
node scripts/test/run-e2e.mjs # храповик known-defects (как в CI)
node scripts/test/run-e2e.mjs --no-known-defects # строгий режим: все дефекты красные
E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs # + откат всех миграций перед запуском
./scripts/test/ci-local.sh # локальный эквивалент verify/format/e2e
```
**Правило храповика.** Исправили дефект → его сценарий позеленел →
`run-e2e.mjs` падает с «дефект исправлен, обновите known-defects.json» —
удалите ID из `tests/e2e/known-defects.json` в том же коммите. Новый красный
сценарий, которого нет в списке, — регрессия.
## 1. Предполагаемый контракт для решений владельца (OWN-01..11) — Codex, этап 4
API пока нет, поэтому сценарии `tests/e2e/30-owner-decisions.e2e.ts` написаны
против наиболее вероятного контракта. Если выбираете другой — поменяйте тесты и
этот раздел в том же PR, смысл проверок (кто что может и когда) менять нельзя.
Все пути относительно `/api/v1/staff`.
### C1. Роли точки/бухгалтера/директора (standalone-аналог Core RBAC)
- `POST /access-grants` (только главный менеджер, `staff:employee:write`)
тело `{ "role": "point_manager" | "accountant" | "director", "org_unit_ids": ["<ULID>"], "display_name": "..." }`
→ `201 { "id", "role", "org_unit_ids", "access_token" }`. Токен показывается один раз,
хранится как хеш (как ключи сотрудников), используется как `Authorization: Bearer`.
- `point_manager` видит/считает только свои `org_unit_ids`; чужая точка → `403`
(и для нового `POST /payroll/batches`, и для старого `POST /payroll/calculate`).
- В platform-режиме те же роли приходят из Core (User ≠ Employee, AGENTS.md).
### C2. Пакет зарплаты точки и согласование
| Метод и путь | Кто | Результат |
| ------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `POST /payroll/batches` `{org_unit_id, from, to}` | accountant; point_manager своей точки | `201 {id, status:"draft", org_unit_id, lines:[{work_session_id, employee_id, org_unit_id, amount_minor}]}` |
| `POST /payroll/batches/:id/submit` | только accountant | `200 {status:"submitted"}`; point_manager → `403`; повтор после подписи → `409` |
| `GET /notifications` | любой пользователь | `200 {notifications:[{type:"payroll.batch.submitted", subject_id:<batchId>}]}`; каждому директору ровно одно уведомление на отправку |
| `POST /payroll/batches/:id/sign` | только director | `200 {status:"signed"}`; accountant → `403`; если смена пакета изменилась после отправки → `409`, пакет получает `status:"returned"` |
| `POST /payroll/batches/:id/return` `{comment}` | director | `200 {status:"returned", return_comment}` |
| `GET /payroll/batches/:id` | accountant, director | `200 {status, return_comment, lines:[...]}` |
Инварианты, которые проверяют тесты:
- до подписи в `staff_payroll_ledger` для сотрудников пакета нет ни `accrual`, ни `payout` (OWN-05);
- подпись атомарно пишет `accrual` и `payout`; после подписи `PATCH /work-sessions/:id`
для смен пакета → `409`, пакет не отправляется повторно (OWN-06);
- строка пакета принадлежит точке, где отработана смена (`staff_work_sessions.org_unit_id`),
а не основной точке сотрудника (OWN-09).
### C3. Согласие на биометрию
- `PUT /employees/:id/biometric-consent` `{ "status": "granted" | "revoked", "document_ref": "..." }`
(главный менеджер) → `200`.
- `GET /terminal/employees/:id/enrollment` (терминальный токен) →
`200 { "employee_id", "registration_allowed": boolean, "reason": "consent_missing" | "consent_revoked" | null }`.
Без записи о согласии — `false/consent_missing` (OWN-10); после отзыва — `false/consent_revoked` (OWN-11).
- Шаблоны отпечатков на сервер не передаются (docs/TERMINAL-API-REQUIREMENTS.md);
сервер хранит только факт согласия. Тот же флаг стоит отдать в `/sync/pull` (employee payload).
Сейчас все OWN красные с `404 Route ... not found` (C1–C3 не реализованы);
OWN-03..09 дополнительно упрутся в N2 (подтверждение смены падает 500).
## 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)` |
## 3. deploy/ (PILOT-1, PILOT-2) — Codex
- PILOT-1: все подстановки в `deploy/pilot/compose.yaml` → `${VAR:?VAR is required}`
(сейчас 14 без `:?`: пустой `STANDALONE_MANAGER_TOKEN` молча превращает pilot в F2).
- PILOT-2: `NODE_ENV: development` в `app`. Конфликт: `config.ts` запрещает
`STAFF_RUNTIME_MODE=standalone` при `NODE_ENV=production`. Нужно решение: либо
разрешить standalone в production при обязательных токенах и HTTPS, либо
отдельный режим для pilot. Тест требует только «не development».
- N6: `scripts/create-app-role.mjs` не работает мигратором без SUPERUSER
(«Only roles with SUPERUSER may change the SUPERUSER attribute»). CI и e2e
обходятся `scripts/test/provision-pg.mjs` (роли создаёт bootstrap-админ, как
`docker/postgres/init-app-role.sh`). В pilot мигратор = `POSTGRES_USER` =
суперпользователь; рекомендую схему как в provision-pg.mjs.
## 4. CI
- Задаче `docker-build` нужен раннер с меткой `docker-host` (Docker daemon).
Без него задача ждёт в очереди; остальные задачи не блокируются.
- `format` красная, пока не исправлен F14 (23 файла на замер 2026-09-26, в основном `src/`, `docs/`, `tests/unit/`).
- Реальный Forgejo-раннер мной не запускался (нет API-токена) — не проверено.
## 5. N13 (гонка первого расчёта) и утечка текста ошибок БД
- `scripts/test/n13-race.mjs`, вывод `tests/e2e/evidence/n13-race.txt`: 10/25/50
параллельных первых `POST /payroll/calculate` × 5 попыток. Как есть — все 500 (N2),
гонку не видно. В одноразовой БД с обходом N2 (`DEFAULT now()` только там) — 0 из 30
некорректных попыток (без смен и с подтверждённой сменой): один `run_id`, один расчёт в БД.
- Почему не воспроизводится (вывод из кода, не проверено отдельным экспериментом):
`calculatePayroll` сначала вызывает `ensureDefaultPayRules`, а там
`INSERT ... ON CONFLICT DO UPDATE` по одним и тем же строкам `staff_pay_rules` тенанта.
Первая транзакция держит блокировки этих строк до коммита, остальные ждут — расчёты
одного тенанта выполняются по очереди случайно. Если upsert правил вынесут из расчёта
или сделают `DO NOTHING`, окно `SELECT ... FOR UPDATE` (строки нет) → `INSERT ... ON
CONFLICT DO NOTHING` со своим `runId` → строки/UPDATE по несуществующему `runId` вернётся.
Рекомендация: явный `pg_advisory_xact_lock` по (tenant, org_unit, period_start) или
`INSERT ... ON CONFLICT DO UPDATE ... RETURNING id`. Скрипт стоит перезапустить после
такого изменения.
- Ответы 500 содержат сырой текст ошибки PostgreSQL (например
`null value in column "occurred_at" of relation "staff_outbox" ...`) — клиенту видна
схема БД. Для 5xx отдавать нейтральное сообщение, подробности — только в лог.