Зачем: 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 по этому замеру.
22 KiB
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.
Зачем
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 |
Как запустить
# 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/ci-local.txt,
JSON — evidence/e2e-results.json, логи серверов — evidence/server-*.txt.
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:
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:
== 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):
::: [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.
[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 — гипотеза по коду.