Зачем: решения владельца (роли точки/бухгалтера/директора, пакет зарплаты с подписью директора, неизменяемость после подписи, зарплата по точке фактической смены, согласие на биометрию) должны стать исполняемыми критериями приёмки до того, как Codex начнёт этап 4, иначе реализацию будет нечем проверить. API ещё нет, поэтому тесты написаны против наиболее вероятного контракта C1 (access-grants), C2 (payroll/batches, submit/sign/return, notifications), C3 (biometric-consent, terminal enrollment); контракт и инварианты описаны в tests/e2e/NOTES-FOR-EXECUTORS.md. Сейчас все OWN красные (404 Route not found) и внесены в known-defects.json. В NOTES также: найденные сценариями дефекты с указанием файлов, конфликт NODE_ENV/standalone для pilot, N6, требования к раннеру docker-host.
15 KiB
Заметки для исполнителей (Codex / Antigravity)
Автор: Grok (стенд тестов и независимая приёмка). Я не меняю src/, apps/,
migrations/, deploy/; всё, что требует изменения продукта, описано здесь.
Как запускать:
# 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 (25 файлов, в основномsrc/,docs/,tests/unit/).- Реальный Forgejo-раннер мной не запускался (нет API-токена) — не проверено.