krona-staff/tests/e2e/NOTES-FOR-EXECUTORS.md
grok 537b784da3 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 15:18:31 +07:00

17 KiB
Raw Blame History

Заметки для исполнителей (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 (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 отдавать нейтральное сообщение, подробности — только в лог.