krona-staff/tests/e2e/NOTES-FOR-EXECUTORS.md
grok bf80b5040e docs(e2e): REPORT/NOTES — d52ecf3, b022885 и исправление сценария
Почему: таблица приёмки и разбор C6 должны соответствовать текущим SHA
веток Codex, а владелец должен видеть, какие расхождения с C6 остались
после d52ecf3 и какая часть красного была ошибкой сценария (типы счётчиков,
d477dde), а не продукта.

- REPORT: таблица по повторному прогону (b022885, d52ecf3), разбор d52ecf3.
- NOTES C6: что закрыл d52ecf3; открыто — многострочный JSON, сумма журнала
  в dry-run, dry-run по умолчанию и обязательные --apply/--actor/--backup-evidence.
- NOTES разд. 8: K18 на b022885 — ротация на ПК есть, на сервере нет,
  личные пути в новых файлах.
2026-09-26 17:20:23 +07:00

315 lines
38 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.

# Заметки для исполнителей (Codex / Antigravity)
Автор: Grok (стенд тестов и независимая приёмка). Я не меняю `src/`, `apps/`,
`migrations/`, `deploy/`; всё, что требует изменения продукта, описано здесь.
Как запускать:
```sh
# 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 дополнительно упрутся в K3/N2 (подтверждение смены падает 500).
ID в названиях тестов — двойные «владелец/мой» (`[K3/N2]`, `[D6/OWN-01]`, `[K17/OWN-10]`,
`[K19/OWN-07]`), по таблице TASK-GROK-2 §2 и AUDIT.md.
### C4. Проверочный пересчёт подписанной ведомости (K8/D1)
- `GET /payroll/runs/:id/verify` (право `staff:payroll:read`) →
`200 { "run_id", "stored_total_minor", "recalculated_total_minor", "matches": boolean }`.
- Пересчёт идёт по снимку версий правил, параметров и ставок, действовавших при подписи
(и ревизиям смен), а не по текущим значениям. После смены ставки `matches` остаётся `true`.
### C5. Параметры пресета в назначении оплаты (K9/D3)
- `PUT /employees/:id/pay-assignment` принимает `params: { <key>: integer }`, ключи — из
`param_declarations` выбранного правила (например, `shift_rate_minor`, `percent_bp`,
`upsell_rate_minor`, `avg_check_target_minor`, `avg_check_bonus_minor` у `waiter_upsell`);
неизвестный ключ → 400. `GET .../pay-assignment` возвращает их в `params`.
- Старые поля `rate_minor`/`percent_bp`/`guarantee_minor` можно оставить как сокращения.
### C6. Перенос арендатора пилота (PILOT-REHOME, TASK-CODEX-2 п.2.2)
Что проверяет `tests/e2e/60-pilot-rehome.e2e.ts` (`[PILOT-REHOME]`):
- Запуск: `DATABASE_ADMIN_URL=<админ БД> node scripts/rehome-tenant.mjs --from 01J00000000000000000000000 --to 01M2R6AC5652NPT3MG34DKN4ZP [--dry-run] [--json]`.
Стенд передаёт `DATABASE_ADMIN_URL` суперпользователя в БД стенда (`E2E_DB_ADMIN_URL` или
`E2E_PG_ADMIN_URL` с именем БД).
- `--json`: последней строкой stdout — `{"tables":{"<таблица>":<строк у --from>},"ledger_amount_minor_sum":<число или строка>}`
(счётчики — число или строка с числом).
Таблицы с нулём можно не печатать.
- `--dry-run`: exit 0, те же числа, в БД ничего не меняется.
- Перенос: exit 0; под `--from` 0 строк во **всех** таблицах с `tenant_id` (список из
`information_schema.columns`, не вручную); у `--to` те же счётчики и та же
`SUM(amount_minor)` в `staff_payroll_ledger`; другие арендаторы не тронуты.
- В `staff_audit_log` у нового арендатора появляется запись с `action`, содержащим `rehome`
(например, `tenant.rehomed`, старый tenant — в `new_values`).
- После переноса `krona_staff_app` видит тех же сотрудников (те же `id`) под новым tenant и никого
под старым; `UPDATE staff_payroll_ledger` ролью приложения по-прежнему отклоняется.
- Повторный запуск (под `--from` пусто / у `--to` уже есть данные) — exit ≠ 0, ничего не меняет.
Что нужно учесть (видно по схеме, `pg_policy`/`pg_constraint` на PG17):
- У `staff_payroll_ledger` нет политики UPDATE/DELETE, у приложения нет права UPDATE, FORCE RLS
действует и на владельца: `krona_migrator` строки журнала не перенесёт. Нужна роль с обходом RLS
(суперпользователь/BYPASSRLS) — поэтому `DATABASE_ADMIN_URL`.
- Внешние ключи составные `(tenant_id, …)` и не DEFERRABLE: переносить родителя раньше детей
нельзя — нужен порядок, отложенные проверки или `session_replication_role = replica` в одной транзакции.
- Политики INSERT `staff_audit_log`, `staff_payroll_ledger`, `staff_attendance_events`,
`staff_timesheet_entries` требуют `actor_id/created_by/updated_by = current_setting('app.user_id')` —
для записи аудита без обхода RLS выставить `app.user_id`.
**Расхождение с `codex/fix-tenant-rehome` (c73bb3b, 16:48 NOVT; ACCEPTANCE-3 с просьбой держаться C6 — 16:35).**
Сценарий на ветке: `dry-run exit: Tenant rehome stopped. Unknown option --from.` По существу скрипт делает
всё, что проверяет сценарий (см. REPORT «Круг 3б»); не совпадает только интерфейс:
| C6 (сценарий) | c73bb3b | Итог в сценарии |
| ------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |
| `--from`, `--to` | `--source`, `--target` | `Unknown option --from`, exit 1 |
| без флага — перенос | нужен `--apply`, без режима — `Specify --dry-run or --apply.` | — |
| — | `--apply` требует `--actor <ULID>` и `--backup-evidence <json>` | у сценария нет ни того, ни другого |
| `--json` | неизвестный флаг → exit 1; JSON печатается всегда | — |
| `DATABASE_ADMIN_URL` | `DATABASE_REHOME_URL` (запасной вариант `DATABASE_MIGRATION_URL`) | — |
| одна строка JSON, числа | многострочный JSON (`JSON.stringify(…, null, 2)`), счётчики — строки | парсер сценария ищет JSON в последней строке |
| `ledger_amount_minor_sum` в dry-run и apply | в dry-run суммы нет; в apply `payrollLedger: [{currency, amountMinor}]` | с адаптером: `expected 'undefined' to be '328228'` |
| `tables` — строки у `--from` | в apply `tables` — у `--to` после переноса (`staff_audit_log` +1) | не проверяется |
**d52ecf3 (17:02)** закрыл строки 1, 4, 5 таблицы: `--from/--to`, `--json`, `DATABASE_ADMIN_URL` принимаются. Без
флага теперь **dry-run** (по C6 — перенос), так что запуск сценария без `--apply` не переносит данные. Открытыми
остаются многострочный JSON (сценарий: `dry-run без JSON-отчёта`), сумма журнала в dry-run и обязательные
`--apply --actor --backup-evidence` (разд. 6.3 в `report-evidence/round3-k18-and-rehome.md`). Типы счётчиков
расходились из-за ошибки сценария; она исправлена в d477dde, сравнение идёт через `Number()`.
Какой интерфейс оставить, решает владелец. Варианты: (а) Codex добавляет в скрипт C6-совместимые
`--from/--to/--json`, `DATABASE_ADMIN_URL`, сумму журнала в dry-run, а `--actor`/`--backup-evidence`
берёт из переменных окружения (например, `REHOME_ACTOR_ID`, `REHOME_BACKUP_EVIDENCE`); (б) владелец утверждает
интерфейс c73bb3b — тогда я меняю сценарий под него отдельным коммитом, а сумму журнала в dry-run
оставляю требованием (TASK-GROK-3 §4 п.3: «`--dry-run` — ничего не меняет, печатает те же числа», то есть числа п.2, включая `SUM(amount_minor)`). В TASK-CODEX-2 §2.2 п.4 dry-run
описан как «только отчёт о количестве строк по таблицам», то есть по этому пункту c73bb3b задание
выполняет, а расходится только с C6.
## 2. Дефекты продукта, найденные сценариями (все красные сейчас)
Команда и фактический вывод — `tests/e2e/evidence/e2e-run.txt`; таблица — `tests/e2e/REPORT.md`.
| ID | Файл | Что исправить |
| ---------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| K4/N1 | `src/portal.ts` POST /employees | `42P08 inconsistent types deduced for parameter $2`: явные `$2::varchar(26)`, `$5::text` или отдельный запрос профессии |
| K3/N2 | `src/portal.ts` PATCH /work-sessions, `src/domain/payroll/service.ts` calculate/approve | `staff_outbox.occurred_at` NULL (23502): передавать `occurred_at` или `DEFAULT now()` новой миграцией |
| K5/N4 | `service.ts` | после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен) |
| K12/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`, верхние границы сумм |
| N12-DATE | `POST /payroll/calculate` | `month=2026-13` → 500 `22008`; проверять месяц 01–12 → 400 (диапазон `2026-02-30` уже даёт 400) |
| K6/D5 | `portal.ts` PATCH /work-sessions | смена подписанной ведомости: `409`, либо сторно в ledger; сейчас (на fix-live-database) 200 и начисление остаётся |
| K7/D2 | `operations.ts` импорт `payroll_entries` | дельта по сумме всех записей дня, вставка по ключу: две точки за день → 222,00 вместо 333,00 ₽; считать дельту по ключу |
| K8/D1 | расчёт/подпись | снимок правил и ставок в подписанной ведомости; контракт проверки C4 (ниже) |
| K9/D3 | PUT pay-assignment | сохранять `params` по `param_declarations` правила (C5 ниже); сейчас `shift_rate_minor` официанта теряется |
| S9 | миграция 0011 FK устройств | `ON DELETE SET NULL` на составном `(tenant_id, org_unit_id)` обнуляет `tenant_id` → 23502; `SET NULL (org_unit_id)` или RESTRICT |
| A4 | обработчик ошибок | в теле 5xx нет SQLSTATE (`code`) и текста PostgreSQL; подробности только в лог |
| K1/F2 | `config.ts`/`identity.ts` | standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions) |
| K10/F3 | POST/DELETE /session | в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию |
| K11/F4 | вход | rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям) |
| F7 | POST /attendance/events | использовать `Idempotency-Key` / `staff_idempotency_keys` |
| F10 | `assertSameOrigin` | мутации по cookie без `Origin` отклонять |
| K13/SYNC-A, K13/SYNC-B | `src/domain/sync/service.ts` | устройство не должно выставлять `confirmed`/`approved` (сейчас `applied`) |
| SYNC-C0 | `pull()` bootstrap | `SELECT ... status FROM staff_org_units` — такого столбца нет → 500 на любой начальной загрузке |
| S5/SYNC-C | `pull()` bootstrap | при `has_more=true` отдаётся `next_cursor=latest`, остаток после 100 сущностей теряется; нужен курсор снимка |
| S5/SYNC-D | портал/импорт | изменения вне sync не пишутся в `staff_sync_change_log` → терминал не узнаёт о новых/изменённых сотрудниках |
| S4/SYNC-E | `executeCommand` | проверять `status='active'` сотрудника |
| S4/SYNC-F | `executeCommand` | сохранять `org_unit_id` в `staff_attendance_events`; payload не может переопределить точку устройства |
| S3/SYNC-G | `push()` | ошибки БД одной команды → `rejected` для неё, а не 500 и откат всего пакета |
| S8/SYNC-H | `/sync/devices` | `assertCookieMutationOrigin` как в портале |
| K1/SYNC-I | `requireManagerContext` | без токенов не выдавать права (как F2) |
| S2/SYNC-J | `executeCommand` | окно времени отметки как в POST /attendance/events (−180 дней … +5 мин) |
| SYNC-K | `acknowledge()` | отклонять курсор больше `MAX(cursor_seq)` |
## 3. deploy/ (K1/PILOT-1, K2/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/`).
- `format` станет зелёной после влития `codex/fix-format-baseline` (проверено на ветке).
- Задача `windows` выключена, пока нет раннера с меткой `windows`: включается переменной
репозитория `KRONA_WINDOWS_RUNNER=true`; для полного e2e нужен секрет
`WINDOWS_E2E_PG_ADMIN_URL` (локальная PostgreSQL 17 на раннере).
- `tests/e2e/evidence/` не хранится в git; в CI это артефакт `e2e-evidence`.
- Реальный 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 отдавать нейтральное сообщение, подробности — только в лог.
## 6. Приёмка веток исполнителей (2026-09-26)
Подробно — `tests/e2e/REPORT.md` и `tests/e2e/report-evidence/acceptance-*.txt`.
- `codex/fix-live-database` (ea68e12, перепроверено на b289dd6): K3/N2 и K4/N1 зелёные на живой PostgreSQL 17.
Дальше по цепочке открылись K5/N4 (196800 вместо 98400 — двойное начисление) и
K6/D5 (отклонение оплаченной смены: 200, начисление осталось). A4 частично: `POST /employees`
больше не 500, но `month=2026-13` по-прежнему отдаёт `code=22008` с текстом.
- `codex/fix-format-baseline` (fd1ec3d): `prettier --check .` чистый. **Но коммит «style»
содержит функциональное исправление Antigravity**: `src/domain/sync/service.ts` и
`tests/unit/sync.test.ts` после нормализации prettier совпадают с
`antigravity/fix-reject-device-shifts` (0 строк diff) и отличаются от 331e490 на 255/139
строк. Изменение поведения стоит вынести в отдельный коммит с объяснением или влить
ветку Antigravity отдельно, иначе история смешивает темы (AGENTS.md).
- `antigravity/fix-reject-device-shifts` (795bba0, перепроверено на 1df2019 — уже на f5ac179):
K13/SYNC-A и K13/SYNC-B зелёные, остальное без изменений.
- Ни одна ветка не влита в основную (f5ac179), поэтому ID из `known-defects.json` не удалены.
- Суммы в тестах кратны рублю: расчёт округляет строку до `rounding_unit_minor`
(по умолчанию 100 коп.). Если округление должно быть другим — это решение продукта, не теста.
## 7. Круг 3 (TASK-GROK-3, 2026-09-26)
Команды и полный вывод — `tests/e2e/REPORT.md` («Круг 3») и `tests/e2e/report-evidence/round3-*.md`.
Приёмка одной командой: `node scripts/test/accept-branch.mjs <ветка>…` (нужен `E2E_PG_ADMIN_URL`
или готовые роли в `E2E_MIGRATOR_URL` + `E2E_APP_URL`).
- **Antigravity, `sync-bootstrap.test.ts` (8baad55)**: `Error: Called end on pool more than once` при
`Tests 2 passed`. Причина: `app.close()` в тесте вызывает хук `onClose` → `database.close()`
(`src/app.ts:148`), затем `afterAll` закрывает тот же `Database` ещё раз. На текущей голове
`24cc18d` файл зелёный (`1 passed (1) | 2 passed (2)`).
- **Antigravity, `sync-rejection.test.ts` (1df2019)**: `column "created_by" of relation "staff_employees"
does not exist` в `beforeAll` (`Tests 2 skipped`). На `6c5bf36`/`b283736` исправлено.
- **Конфликты**: на прежних головах `1df2019 × 8baad55` и `8baad55 × 513cdff` конфликтовали в
`src/domain/sync/service.ts`; на текущих головах (fetch 16:16 NOVT) конфликтов нет ни в одной из 55 пар.
- **Codex, `codex/fix-format-baseline`**: на `513cdff` в `service.ts`/`sync.test.ts` было 232/130 строк сверх
prettier, все есть в `antigravity/fix-reject-device-shifts`; на `a05777c` — только форматирование (19 файлов).
- **Codex, `live-postgres-remediation.test.ts`**: выполняется только при `DATABASE_TEST_ISOLATED=true`,
`DATABASE_TEST_GUARD_TOKEN`, таблице `krona_staff_test_guard` с этим маркером и имени БД
`krona_staff_audit_*`. Соглашение нигде, кроме самого теста, не описано, и ни CI, ни
`scripts/test/*` его не выполняют — в `npm run check` файл всегда `skipped`. `accept-branch` ставит
предохранитель в одноразовой БД: `1 passed (1)`. Предложение: описать соглашение в README/AGENTS
и после слияния включить его в CI (могу сделать в своей зоне, `.forgejo/workflows/ci.yml`).
- **N5 до сих пор в основе**: `tenant-isolation.test.ts` красный на `b0243d5` и всех ветках от
`f5ac179` (`permission denied for table staff_employees`); исправление — в `grok/e2e-harness`.
Локальная сборка основа + fix-live-database + fix-sync-c0-bootstrap + grok/e2e-harness:
интеграционные 7/7, храповик просит убрать ровно K3/N2, K4/N1, K13/SYNC-A, K13/SYNC-B, SYNC-C0.
- **Схема (низкий приоритет)**: `staff_notification_deliveries.outbox_id → staff_outbox(id)` — единственный
внешний ключ без `tenant_id`; все остальные составные `(tenant_id, …)`. База не мешает доставке
ссылаться на событие outbox другого арендатора (RLS фильтрует чтение, но не ссылку). Предложение:
`UNIQUE (tenant_id, id)` на `staff_outbox` и FK `(tenant_id, outbox_id)`.
- **PILOT-REHOME**: скрипта нет → `[PILOT-REHOME]` красный; контракт — C6.
## 8. Круг 3б (основа f7d3c73, 2026-09-26 ~17:00 NOVT)
Вывод — `tests/e2e/report-evidence/round3-accept-f7d3c73.md`, `round3-k18-and-rehome.md`.
- **codex/fix-tenant-config (535052e)**: `npm run check` красный —
`tests/unit/sync.test.ts:556:18` → `STANDALONE_TENANT_ID uses the development placeholder…`
(строка 553 — заглушка, 563 — `NODE_ENV: 'test'`). Вывод владельца подтверждён; как исправить —
ACCEPTANCE-3 (разрешить заглушку при `NODE_ENV === 'test'`).
- **codex/fix-tenant-rehome (c73bb3b → d52ecf3)**: по существу перенос верный (копия после восстановления: счётчики,
ledger, FK, аудит `tenant.rehomed`, изоляция, отказ повтора). `[PILOT-REHOME]` красный из-за интерфейса —
таблица различий в C6. Интеграционный тест `rehome-tenant.test.ts` зелёный только с
`DATABASE_REHOME_TEST_URL`/`DATABASE_REHOME_TEST_CONFIRM` — в `npm run check` и CI он всегда `skipped`.
Тест требует подключения суперпользователя (`rehome-tenant.test.ts:92-93`) и содержит один `it` —
запуск скрипта от не-суперпользователя (как `krona_migrator` в пилоте) не покрыт.
- **codex/fix-pilot-backups (a005c5e → b022885, K18)**: `test-backup.sh`/`test-backup-pull.sh` проходят (моки), но в
`npm run check` и CI не входят. Учебное восстановление без Docker прошло (шим docker → pg_dump PG17,
CMS, sha256, pg_restore, `restore-drill-report.sql` совпал). Замечания:
- `backup.sh` снимает только `pg_dump -Fc` одной БД: ролей (globals) в копии нет. В кластере без ролей
`pg_restore` → exit 1, 61 ошибка `role "krona_migrator" does not exist`. Шаг 4 runbook («Restore required
roles and grants») нужно сделать исполнимым: скрипт создания ролей для восстановления или
`pg_dumpall --globals-only --no-role-passwords` рядом с копией.
- `restore-drill-isolation.sql` жёстко зашит на `01J00000000000000000000000` → `01M2R6AC5652NPT3MG34DKN4ZP`;
после переноса «source/target» поменяются местами — стоит параметризовать (`psql -v`).
- В `docs/BACKUP-RESTORE.md` личный путь `C:/Users/trush/Documents/KronaStaffBackups/` — в репозитории
лучше указать роль или место без имени пользователя.
- b022885: ротация копий на ПК есть (`BACKUP_LOCAL_RETENTION_DAYS`, 90 дней; в `test-backup-pull.sh` есть проверка),
ротации на сервере нет. В новых `scripts/run-pilot-backup-pull.ps1` и `deploy/pilot/backup.home-server.example`
тоже зашиты личные пути. Контроль отказов таймера и задачи pull пока описан только в документе.
- Предложение (моя зона, после слияния): добавить в CI `bash scripts/test-backup.sh`,
`bash scripts/test-backup-pull.sh` и запуск `rehome-tenant.test.ts` / `live-postgres-remediation.test.ts`
на одноразовых БД — так же, как это делает `accept-branch`.