# PR: e2e-стенд на реальной PostgreSQL 17 и независимая приёмка (grok/e2e-harness) База: `codex/bootstrap-platform-foundation` @ `778ba2d`. Сравнение: Изменены только `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://:@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 — гипотеза по коду.