krona-staff/tests/e2e/REPORT.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

225 lines
22 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.

# PR: e2e-стенд на реальной PostgreSQL 17 и независимая приёмка (grok/e2e-harness)
База: `codex/bootstrap-platform-foundation` @ `778ba2d`. Сравнение:
<https://git.ochenstarik.ru/ochenstarik/krona-staff/compare/codex/bootstrap-platform-foundation...grok/e2e-harness>
Изменены только `tests/e2e/**`, `tests/integration/tenant-isolation.test.ts`,
`.forgejo/workflows/ci.yml`, `scripts/test/**`. `src/`, `apps/`, `migrations/`, `deploy/`,
`package.json` не тронуты. Всё, что требует правки продукта, — в
[`NOTES-FOR-EXECUTORS.md`](NOTES-FOR-EXECUTORS.md).
## Зачем
156 зелёных тестов на поддельной БД не заметили, что создание сотрудника,
подтверждение смены и расчёт зарплаты на реальной PostgreSQL падают всегда. Этот PR
добавляет стенд, где реальный сервер (`tsx src/server.ts`) работает с реальной
PostgreSQL 17 под ролью приложения без привилегий, миграции применяет мигратор без
SUPERUSER, а все токены и пароли случайные (`crypto.randomBytes`). Каждый сценарий
проверяет **правильное** поведение, поэтому красный цвет означает открытый дефект.
## Источник ID
Файлы владельца (`AUDIT.md`, `live-audit.mjs`, `live-sync.mjs`) до конца работы в
`/workspace/uploads/krona-staff-owner/` не появились. Сценарии написаны по моему
аудиту `2026-09-26_full-project-audit_grok_3e20544.md` (ID `F*`/`N*`) и по коду. ID владельца
K1, K3, K4, K5, K6, K10, K11, K12, S1, S2, S9, A4 **не сопоставлены — не проверено**.
Синхронизация помечена `SYNC-*` (мои ID, вероятно пересекаются с S1–S9 владельца).
Когда файлы владельца будут доступны, достаточно переименовать теги в названиях
тестов (например `[K3/N1]`) и ключи в `known-defects.json`.
## Коммиты
| SHA | Что |
| --------- | ------------------------------------------------------------------------------------- |
| `29fefc1` | `scripts/test/provision-pg.mjs`, `migrate-roundtrip.mjs`: роли как в pilot, roundtrip |
| `88fc749` | N5: очистка tenant-isolation ролью мигратора |
| `d7e9008` | prettier для tenant-isolation.test.ts |
| `cf7f6f4` | стенд e2e: global-setup, клиент, фикстуры, храповик `run-e2e.mjs` |
| `518494d` | сценарии основного API (N1–N4, N9–N12, F2–F4, F7, F10) |
| `32ced4b` | сценарии `/sync/*` (SYNC-A…L) |
| `9bdfb66` | форматирование сгенерированных evidence |
| `f59be3a` | CI: PostgreSQL 17, роли, roundtrip, e2e, docker-build отдельно, SHA-пины |
| `a19c06b` | проверка `deploy/pilot/compose.yaml` (PILOT-1..3) |
| `1e325a6` | сценарии решений владельца (OWN-01..11) + заметки исполнителям |
| `3388b90` | N13: гонка первого расчёта (скрипт и замер) |
| (этот) | REPORT.md и свежие evidence |
## Как запустить
```sh
# PostgreSQL 17, bootstrap-админ нужен только для создания ролей и БД
export E2E_PG_ADMIN_URL=postgres://<admin>:<pw>@127.0.0.1:55433/postgres
node scripts/test/run-e2e.mjs # храповик known-defects (как в CI)
node scripts/test/run-e2e.mjs --no-known-defects # строго: любой красный = exit 1
E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs
./scripts/test/ci-local.sh # локальный эквивалент всех задач CI
```
Роли (`provision-pg.mjs`): `krona_migrator` — NOSUPERUSER NOCREATEDB NOCREATEROLE,
владелец БД и схемы `public`; `krona_staff_app` — NOSUPERUSER NOBYPASSRLS, права выдают
миграции 0003/0004. Пароли случайные, пишутся в env-файл 0600 во временном каталоге,
который удаляется после запуска.
## Таблица green/red по ID
Команда: `E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs` (в составе
`./scripts/test/ci-local.sh`), 2026-09-26 14:33 (UTC+7), PostgreSQL 17.11. Фактический вывод:
[`evidence/e2e-run.txt`](evidence/e2e-run.txt), [`evidence/ci-local.txt`](evidence/ci-local.txt),
JSON — [`evidence/e2e-results.json`](evidence/e2e-results.json), логи серверов — `evidence/server-*.txt`.
```text
Test Files 4 failed | 1 passed (5)
Tests 38 failed | 9 passed (47)
[run-e2e] OK: стенд работает, все красные сценарии — известные открытые дефекты
```
| ID | Цвет | Что проверяется (правильное поведение) | Фактически (из вывода) |
| ------------ | ----- | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| HARNESS (×5) | GREEN | live/ready, PG17, app-роль без SUPERUSER/BYPASSRLS, токены, терминал | 5/5 passed |
| CTRL (×2) | GREEN | RBAC терминала/сотрудника, уволенный теряет доступ | 2/2 passed |
| N1 | RED | POST /employees → 201 | 500 `42P08 inconsistent types deduced for parameter $2` |
| N2 | RED | подтверждение смены, расчёт, утверждение → 200 | 500 `23502 null value in column "occurred_at" of "staff_outbox"` |
| N4 | RED | пересекающиеся расчёты не начисляют смену дважды | блокирован N2 (500 23502 на предусловии) — сам N4 **не проверен** |
| N3 | RED | push на внутренний адрес отклоняется, соединений нет | 201 и 1 входящее соединение на 127.0.0.1 |
| N9 | RED | чужой push-endpoint не перепривязывается | 201 |
| N10 | RED | терминал не читает справочники (403) | professions 200, org-units 200 |
| N11 | RED | импорт с неверным kind → 400 | 500 `23514` check constraint staff_payroll_ledger |
| N12 | RED | лишние поля/нереальные суммы → 400 | 200 |
| F2 | RED | процесс без токенов не даёт прав анониму (401) | /portal 200, /payroll 200, POST /professions 201 |
| F3 | RED | cookie без сырого токена, logout отзывает | `rawTokenInCookie: true` |
| F4 | RED | 429 не позже 30 неверных входов | все 401, 429 нет |
| F7 | RED | повтор Idempotency-Key с другим телом не создаёт событие | второе событие создано (201) |
| F10 | RED | мутация по cookie без Origin → 403 | 201 |
| SYNC-A | RED | устройство не подтверждает смену | `applied` |
| SYNC-B | RED | устройство не утверждает табель | `applied` |
| SYNC-C0 | RED | bootstrap pull без курсора → 200 | 500 `42703 column "status" does not exist` (новая находка) |
| SYNC-C | RED | bootstrap >100 сущностей постранично без потерь | 500 42703 (блокирован SYNC-C0) |
| SYNC-D | RED | изменения из портала видны в инкрементальном pull | изменения нет |
| SYNC-E | RED | отметка уволенного через sync отклоняется | `applied` |
| SYNC-F | RED | точка устройства сохраняется, чужая точка в payload не принимается | `applied`, точка не сохранена |
| SYNC-G | RED | битая команда отклоняется по отдельности | 500 `22007` на весь пакет |
| SYNC-H | RED | регистрация устройства по cookie с чужим Origin → 403 | 201 |
| SYNC-I | RED | без токенов анонимно устройство не регистрируется | 201 |
| SYNC-J | RED | отметка 2001 года через sync отклоняется | `applied` |
| SYNC-K | RED | ack курсора из будущего → 400 | 200 |
| SYNC-L | GREEN | отозванное устройство не синхронизируется (403) | 403 (позитивный контроль) |
| OWN-01 | RED | менеджер точки A считает зарплату точки B → 403 | 404 `Route POST:/api/v1/staff/access-grants not found` (нет API) |
| OWN-02 | RED | менеджер отправляет пакет на согласование → 403 | 404 access-grants (нет API) |
| OWN-03 | RED | бухгалтер отправляет пакет → успех, каждому директору 1 уведомление | 404 access-grants (нет API) |
| OWN-04 | RED | бухгалтер подписывает пакет → 403 | 404 access-grants (нет API) |
| OWN-05 | RED | до подписи в ledger нет начислений/выплат | 404 access-grants (нет API) |
| OWN-06 | RED | подпись директора → ledger, пакет и смены неизменяемы | 404 access-grants (нет API) |
| OWN-07 | RED | смена изменена после отправки → 409, пакет у бухгалтера | 404 access-grants (нет API) |
| OWN-08 | RED | возврат с комментарием → пакет у бухгалтера, комментарий сохранён | 404 access-grants (нет API) |
| OWN-09 | RED | смена сотрудника A в точке B → в расчёт точки B | 404 access-grants (нет API) |
| OWN-10 | RED | без согласия на биометрию → терминалу «регистрация запрещена» | 404 `GET .../terminal/employees/:id/enrollment` (нет API) |
| OWN-11 | RED | согласие отозвано → статус для терминала меняется | 404 `PUT .../biometric-consent` (нет API) |
| PILOT-1 | RED | все подстановки в pilot compose — `${VAR:?}` | 14 переменных без `:?` (первая — `STANDALONE_MANAGER_TOKEN`) |
| PILOT-2 | RED | нет `NODE_ENV=development` | сервис `app` |
| PILOT-3 | GREEN | у `app` нет кредов мигратора | passed |
| N13 | — | гонка первого расчёта (скрипт, не vitest) | см. ниже: как есть 15/15 → 500 (N2); с обходом N2 — 0/30 гонок |
| N5 | GREEN | tenant-isolation на живой БД | до: FAIL; после: 2/2 прогона exit=0 (см. ниже) |
Предполагаемые контракты для OWN-01..11 (C1 роли точки/бухгалтера/директора, C2 пакет
зарплаты и согласование, C3 согласие на биометрию) описаны в `NOTES-FOR-EXECUTORS.md` §1.
### Механизм для красных сценариев (храповик)
Выбран список `tests/e2e/known-defects.json` и одна задача `e2e`, а не отдельная
задача «allowed to fail». Причины: задача с `continue-on-error` зеленеет при любом
сбое, включая сломанный стенд, а храповик различает три случая: (1) стенд сломан
(`[HARNESS]`/`[CTRL]` красные или тест без ID) → exit 1; (2) новый красный ID не из
списка (регрессия) → exit 1; (3) ID из списка стал зелёным → exit 1 с просьбой
вычеркнуть его (исправление фиксируется, откат снова станет красным). Строгий режим
`--no-known-defects` показывает полную картину.
## N5: tenant-isolation до/после
До (база 778ba2d, `DATABASE_TEST_URL` = роль приложения), [`evidence/n5-before.txt`](evidence/n5-before.txt):
```text
FAIL tests/integration/tenant-isolation.test.ts > PostgreSQL tenant isolation
error: permission denied for table staff_employees
❯ tests/integration/tenant-isolation.test.ts:40:7
Test Files 1 failed (1)
Tests 1 passed (1)
exit=1
```
После (очистка соединением мигратора `DATABASE_TEST_MIGRATION_URL`, с
`app.tenant_id` из-за FORCE RLS, до и после теста), [`evidence/n5-after.txt`](evidence/n5-after.txt):
```text
== run 1 ... Test Files 1 passed (1) Tests 1 passed (1) exit=0
== run 2 ... Test Files 1 passed (1) Tests 1 passed (1) exit=0
```
Полный `npm run check` с тестовой БД (задача verify в `ci-local.sh`):
`Test Files 19 passed (19)`, `Tests 188 passed (188)`.
## CI (`.forgejo/workflows/ci.yml`)
- `verify`: сервис `postgres:17-alpine@sha256:18cfe3ef…` (bootstrap-пользователь только
для provision), роли как в pilot (мигратор без SUPERUSER, app без BYPASSRLS),
миграции `up → откат всех 11 → up → up (no-op)`, `npm run check` с
`DATABASE_TEST_URL`/`DATABASE_TEST_MIGRATION_URL` (tenant-isolation реально
выполняется), `npm audit --audit-level=high`.
- `format`: отдельно, чтобы F14 (23 файла вне моих путей) не маскировал остальное.
- `e2e`: тот же сервис PostgreSQL 17, `E2E_MIGRATIONS=roundtrip`, храповик.
- `docker-build`: отдельная задача на метке `docker-host`, `needs: verify`, только push.
- Все actions закреплены SHA (F13): `actions/checkout@11d5960a… # v4.4.0`,
`actions/setup-node@49933ea5… # v4.4.0` (SHA получены `git ls-remote`, совпадают на
github.com и code.forgejo.org).
Валидация: `actionlint -config-file actionlint.yaml .forgejo/workflows/ci.yml` (1.7.12,
метки `docker`, `docker-host`) → 0 замечаний, exit 0. Локальный эквивалент
`./scripts/test/ci-local.sh` ([`evidence/ci-local.txt`](evidence/ci-local.txt)):
```text
::: [migrate-roundtrip] OK (== roundtrip OK: rolledBack=11 reapplied=11)
::: [check] OK (Test Files 19 passed, Tests 188 passed)
::: [audit] OK (found 0 vulnerabilities)
::: [format-check] FAIL exit=1 (Code style issues found in 23 files — F14)
::: [e2e] OK (храповик: все красные — известные дефекты)
=== итог: status=1 (только из-за format)
```
## Проверка pilot compose (PILOT-1..3)
`tests/e2e/40-compose-pilot.e2e.ts` читает `deploy/pilot/compose.yaml` (только чтение).
Положен в `tests/e2e/`, а не рядом с существующим тестом корневого compose
(`tests/unit/deployment.test.ts`), потому что `tests/unit/` вне разрешённых путей.
Результат: PILOT-1 RED (14 подстановок без `:?`), PILOT-2 RED (`NODE_ENV: development` в
`app`; конфликт с запретом standalone в production в `config.ts` описан в заметках),
PILOT-3 GREEN.
## N13: гонка первого расчёта
`node scripts/test/n13-race.mjs` — 10/25/50 параллельных первых `POST /payroll/calculate`,
5 попыток на размер, каждая попытка на новой точке. Вывод: [`evidence/n13-race.txt`](evidence/n13-race.txt).
```text
[n13] ИТОГ mode=as-is sessions=false: попыток 15, некорректных 15 (все 500: N2, occurred_at)
[n13] ИТОГ mode=n2-bypass sessions=false: попыток 15, некорректных 0 (200×N, 1 run_id, 1 расчёт)
[n13] ИТОГ mode=n2-bypass sessions=true: попыток 15, некорректных 0 (с подтверждённой сменой, db_lines=1)
```
`n2-bypass` — диагностика: в одноразовой БД скрипта мигратор ставит
`DEFAULT now()` для `staff_outbox.occurred_at`; продукт и миграции не меняются.
Вердикт: **гонка не воспроизведена (0 из 30 попыток при 10/25/50)**, совпадает с
«N13 not reproduced» владельца. Вероятная причина (из кода, отдельно не проверено):
upsert `ensureDefaultPayRules` в начале расчёта случайно сериализует расчёты одного
тенанта. Окно в коде остаётся — рекомендация в заметках §5.
## Не проверено
- Реальный Forgejo-раннер: workflow не запускался на сервере (нет API-токена, задачи
выполнены локально эквивалентным скриптом). Раннера с меткой `docker-host` может не быть.
- `docker build` не запускался (на этой машине нет Docker).
- Windows-only: DPAPI, ACL каталога данных, установщик, WinUI, реальный named pipe —
не проверено; см. тесты Codex в `apps/KronaStaffTerminal.Tests`.
- Сопоставление с ID владельца K1/K3/K4/K5/K6/K10/K11/K12, S1/S2/S9, A4 и сверка с его
`live-audit.mjs`/`live-sync.mjs` — файлов нет.
- N4 и SYNC-C по существу: блокированы N2 и SYNC-C0 соответственно.
- OWN-01..11 проверяют предполагаемый контракт; реальный контракт Codex может отличаться.
- Причина невоспроизводимости N13 — гипотеза по коду.