docs(e2e): доказательства — приёмка на f7d3c73, K18 и перенос арендатора

Почему: владелец просил прогнать все открытые ветки против новой основы f7d3c73, включая
codex/fix-tenant-config, codex/fix-tenant-rehome, codex/fix-pilot-backups, и проверить K18 и
scripts/rehome-tenant.mjs фактами.
round3-accept-f7d3c73.md — вывод accept-branch (7 веток, 28 пар merge-tree — конфликтов нет).
round3-k18-and-rehome.md — тесты Codex для backup; учебное восстановление без Docker (шим docker →
локальный pg_dump PG17, CMS-шифрование, sha256, pg_restore, restore-drill-report.sql совпал);
перенос заглушки на восстановленной копии интерфейсом продукта; [PILOT-REHOME] через
диагностический адаптер (не в продукте, не в сценарии) — исходники скриптов приложены.
This commit is contained in:
grok 2026-09-26 17:12:07 +07:00
parent 18f944e3c3
commit 43b684e04f
2 changed files with 391 additions and 0 deletions

View file

@ -0,0 +1,101 @@
# Команда (после git fetch, 16:59 NOVT; база f7d3c73): node scripts/test/accept-branch.mjs $(ветки origin/codex/_, origin/antigravity/_, которые не предки основы)
# = antigravity/fix-reject-device-shifts antigravity/fix-sync-c0-bootstrap codex/fix-format-baseline codex/fix-live-database codex/fix-pilot-backups codex/fix-tenant-config codex/fix-tenant-rehome
# Предки основы (не прогонялись): antigravity/employee-notifications, antigravity/offline-sync-backend, codex/finance-calendar-dashboard, codex/frontend-library, codex/security-finance-production, codex/server-web-production
## accept-branch — 2026-09-26 10:02 UTC
База: `origin/codex/bootstrap-platform-foundation` = f7d3c73; стенд (оверлей tests/e2e, scripts/test): 18f944e; БД: роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep. Логи: `tests/e2e/evidence/accept/2026-09-26T09-59-10/`.
### Сводка
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
| ------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------- | ------------------ | ------------------------------- |
| origin/codex/bootstrap-platform-foundation (база) | f7d3c73 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | n/a | n/a |
| origin/antigravity/fix-reject-device-shifts | b283736 | OK (196 passed | 3 skipped (199)) | 4/5 зелёных; FAIL: tenant-isolation.test.ts | 44/7 | K13/SYNC-A, K13/SYNC-B | — |
| origin/antigravity/fix-sync-c0-bootstrap | 24cc18d | OK (197 passed | 5 skipped (202)) | 5/6 зелёных; FAIL: tenant-isolation.test.ts | 43/8 | K13/SYNC-A, K13/SYNC-B, SYNC-C0 | — |
| origin/codex/fix-format-baseline | a05777c | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | — | — |
| origin/codex/fix-live-database | 1573860 | OK (197 passed | 2 skipped (199)) | 4/5 зелёных; FAIL: tenant-isolation.test.ts | 44/7 | K3/N2, K4/N1 | — |
| origin/codex/fix-pilot-backups | a005c5e | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | — | — |
| origin/codex/fix-tenant-config | 535052e | FAIL exit=1: Error: STANDALONE_TENANT_ID uses the development placeholder, which is allowed only in development on loopback | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | — | — |
| origin/codex/fix-tenant-rehome | c73bb3b | OK (197 passed | 2 skipped (199)) | 4/5 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | — | — |
### Интеграционные тесты (каждый файл отдельно, свежая БД)
| Ветка | Файл | Итог | Test Files | Tests | Первая ошибка |
| ------------------------------------------- | --------------------------------- | ---- | ------------ | -------------- | -------------------------------------------------- |
| origin/codex/bootstrap-platform-foundation | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/codex/bootstrap-platform-foundation | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/bootstrap-platform-foundation | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/codex/bootstrap-platform-foundation | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/antigravity/fix-reject-device-shifts | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/antigravity/fix-reject-device-shifts | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/antigravity/fix-reject-device-shifts | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/antigravity/fix-reject-device-shifts | sync-rejection.test.ts | OK | 1 passed (1) | 2 passed (2) | |
| origin/antigravity/fix-reject-device-shifts | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/antigravity/fix-sync-c0-bootstrap | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/antigravity/fix-sync-c0-bootstrap | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/antigravity/fix-sync-c0-bootstrap | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/antigravity/fix-sync-c0-bootstrap | sync-bootstrap.test.ts | OK | 1 passed (1) | 2 passed (2) | |
| origin/antigravity/fix-sync-c0-bootstrap | sync-rejection.test.ts | OK | 1 passed (1) | 2 passed (2) | |
| origin/antigravity/fix-sync-c0-bootstrap | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/codex/fix-format-baseline | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/codex/fix-format-baseline | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-format-baseline | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/codex/fix-format-baseline | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/codex/fix-live-database | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/codex/fix-live-database | live-postgres-remediation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-live-database | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-live-database | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/codex/fix-live-database | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/codex/fix-pilot-backups | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/codex/fix-pilot-backups | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-pilot-backups | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/codex/fix-pilot-backups | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/codex/fix-tenant-config | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/codex/fix-tenant-config | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-tenant-config | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/codex/fix-tenant-config | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
| origin/codex/fix-tenant-rehome | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
| origin/codex/fix-tenant-rehome | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-tenant-rehome | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
| origin/codex/fix-tenant-rehome | rehome-tenant.test.ts | OK | 1 passed (1) | 1 passed (1) | |
| origin/codex/fix-tenant-rehome | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
### Попарные слияния (`git merge-tree --write-tree`)
| Ветка A | Ветка B | Результат | Конфликтные файлы |
| ------------------------------------------- | ------------------------------------------- | --------- | ----------------- |
| origin/codex/bootstrap-platform-foundation | origin/antigravity/fix-reject-device-shifts | чисто | |
| origin/codex/bootstrap-platform-foundation | origin/antigravity/fix-sync-c0-bootstrap | чисто | |
| origin/codex/bootstrap-platform-foundation | origin/codex/fix-format-baseline | чисто | |
| origin/codex/bootstrap-platform-foundation | origin/codex/fix-live-database | чисто | |
| origin/codex/bootstrap-platform-foundation | origin/codex/fix-pilot-backups | чисто | |
| origin/codex/bootstrap-platform-foundation | origin/codex/fix-tenant-config | чисто | |
| origin/codex/bootstrap-platform-foundation | origin/codex/fix-tenant-rehome | чисто | |
| origin/antigravity/fix-reject-device-shifts | origin/antigravity/fix-sync-c0-bootstrap | чисто | |
| origin/antigravity/fix-reject-device-shifts | origin/codex/fix-format-baseline | чисто | |
| origin/antigravity/fix-reject-device-shifts | origin/codex/fix-live-database | чисто | |
| origin/antigravity/fix-reject-device-shifts | origin/codex/fix-pilot-backups | чисто | |
| origin/antigravity/fix-reject-device-shifts | origin/codex/fix-tenant-config | чисто | |
| origin/antigravity/fix-reject-device-shifts | origin/codex/fix-tenant-rehome | чисто | |
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-format-baseline | чисто | |
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-live-database | чисто | |
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-pilot-backups | чисто | |
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-tenant-config | чисто | |
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-tenant-rehome | чисто | |
| origin/codex/fix-format-baseline | origin/codex/fix-live-database | чисто | |
| origin/codex/fix-format-baseline | origin/codex/fix-pilot-backups | чисто | |
| origin/codex/fix-format-baseline | origin/codex/fix-tenant-config | чисто | |
| origin/codex/fix-format-baseline | origin/codex/fix-tenant-rehome | чисто | |
| origin/codex/fix-live-database | origin/codex/fix-pilot-backups | чисто | |
| origin/codex/fix-live-database | origin/codex/fix-tenant-config | чисто | |
| origin/codex/fix-live-database | origin/codex/fix-tenant-rehome | чисто | |
| origin/codex/fix-pilot-backups | origin/codex/fix-tenant-config | чисто | |
| origin/codex/fix-pilot-backups | origin/codex/fix-tenant-rehome | чисто | |
| origin/codex/fix-tenant-config | origin/codex/fix-tenant-rehome | чисто | |
### Форматирование без поведения
**origin/codex/fix-format-baseline** (a05777c), merge-base f5ac179, изменено файлов: 19 — ТОЛЬКО ФОРМАТИРОВАНИЕ

View file

@ -0,0 +1,290 @@
# K18 (codex/fix-pilot-backups a005c5e) и перенос арендатора (codex/fix-tenant-rehome c73bb3b) на восстановленной копии
# PG17 17.11 локально, без Docker. Ветки исполнителей не менялись: отдельные detached worktree
# /workspace/krona-staff-acceptance/diag-pilot-backups и diag-tenant-rehome.
## 1. Тесты Codex из ветки (моки docker/ssh/scp)
$ bash scripts/test-backup.sh; bash scripts/test-backup-pull.sh
backup tests passed: syntax, CMS round-trip, pg_dump/OpenSSL/scp failures, no plaintext dump on disk (exit=0)
backup pull tests passed: CMS+checksum, idempotence, stale/mismatch rejection, path validation, scp failure (exit=0)
## 2. Учебное восстановление: настоящий pg_dump → backup.sh → sha256 → расшифровка → pg_restore → отчёты
# docker подменён шимом: 'docker compose exec -T postgres sh -eu -c <cmd>' выполняет <cmd> локально
# (pg_dump -U pgadmin -d krona_k18_src -Fc); источник — копия БД e2e (заглушка, посторонние арендаторы, данные e2e).
$ bash drill.sh
== 1. источник: копия krona_acc_e2e (данные последнего e2e-прогона accept-branch)
== 2. deploy/pilot/backup.sh (external-pull)
backup=<tmp>/server/backups/krona-staff-20260926T101055Z-1837466.dump.cms
checksum=<tmp>/server/backups/krona-staff-20260926T101055Z-1837466.dump.cms.sha256
off_host_copy=pending_pull
exit=0
plaintext на сервере: 0 файлов
первые байты (DER): 30 80 06 09
== 3. «перенос на ПК» (cp вместо ssh/scp — pull-pilot-backups.sh здесь не запускался) + sha256
krona-staff-20260926T101055Z-1837466.dump.cms: OK
== 4. расшифровка ключом вне сервера + pg_restore в новую БД
TABLE DATA в дампе: 28
pg_restore exit=0
== 5. restore-drill-report.sql: источник vs копия
строк отчёта: 79; diff:
идентичны
RLS|27|t
LEDGER|<ulid>|RUB|263568
LEDGER|<ulid>|RUB|22200
LEDGER|<ulid>|RUB|329705
schema_migrations: 11
staff_payroll_ledger owner=krona_migrator
гранты krona_staff_app: 100
== 6. restore-drill-isolation.sql (krona_staff_app, NOBYPASSRLS)
BEGIN
SET
SET
source-employee-count|3
SET
target-employee-count|0
ROLLBACK
## 3. Перенос заглушки на копии, интерфейс продукта как есть
$ bash rehome-on-copy.sh # DATABASE_REHOME_URL=<суперпользователь>@krona_k18_restore
== dry-run: node scripts/rehome-tenant.mjs --dry-run --source <STUB> --target <TARGET>
exit=0
{"mode":"dry-run","source":"<STUB>","target":"<TARGET>","tables":{"staff_attendance_events":"2","staff_audit_log":"2","staff_daily_metrics":"2","staff_employees":"3","staff_idempotency_keys":"2","staff_inbox":"2","staff_metric_definitions":"2","staff_notification_deliveries":"2","staff_org_unit_terms":"2","staff_org_units":"2","staff_outbox":"2","staff_pay_assignments":"2","staff_pay_rules":"2","staff_payouts":"2","staff_payroll_ledger":"3","staff_payroll_lines":"2","staff_payroll_runs":"2","staff_professions":"2","staff_push_subscriptions":"2","staff_standalone_employee_access":"2","staff_sync_change_log":"2","staff_sync_command_inbox":"2","staff_sync_device_checkpoints":"2","staff_sync_devices":"2","staff_timesheet_entries":"2","staff_work_items":"2","staff_work_sessions":"2"}}
dry-run: БД не изменилась
== C6-интерфейс на этом же скрипте
Tenant rehome stopped. Unknown option --from. No connection details or row contents were printed.
exit=1
== apply без --backup-evidence
Tenant rehome stopped. --backup-evidence is required for --apply. No connection details or row contents were printed.
exit=1
== apply: --apply --source <STUB> --target <TARGET> --actor <ulid> --backup-evidence evidence.json
exit=0
{"mode":"apply","source":"<STUB>","target":"<TARGET>","tables":{"staff_attendance_events":"2","staff_audit_log":"3","staff_daily_metrics":"2","staff_employees":"3","staff_idempotency_keys":"2","staff_inbox":"2","staff_metric_definitions":"2","staff_notification_deliveries":"2","staff_org_unit_terms":"2","staff_org_units":"2","staff_outbox":"2","staff_pay_assignments":"2","staff_pay_rules":"2","staff_payouts":"2","staff_payroll_ledger":"3","staff_payroll_lines":"2","staff_payroll_runs":"2","staff_professions":"2","staff_push_subscriptions":"2","staff_standalone_employee_access":"2","staff_sync_change_log":"2","staff_sync_command_inbox":"2","staff_sync_device_checkpoints":"2","staff_sync_devices":"2","staff_timesheet_entries":"2","staff_work_items":"2","staff_work_sessions":"2"},"auditRowsAdded":1,"payrollLedger":[{"currency":"RUB","amountMinor":"263568"}]}
== сверка (restore-drill-report.sql до/после; счётчики по таблице/арендатору, RLS, ledger)
-- строки с <STUB> после: 0
-- прочие арендаторы до/после идентичны: да
-- счётчики <STUB> до vs <TARGET> после (кроме audit):
совпадают
-- audit: до 2, после 3
LEDGER|<TARGET>|RUB|263568
LEDGER|<other>|RUB|22200
LEDGER|<other>|RUB|329705
до: LEDGER|<STUB>|RUB|263568
до: LEDGER|<other>|RUB|22200
до: LEDGER|<other>|RUB|329705
tenant.rehomed tenant_rehome
FK после: 30
== krona_staff_app
source-employee-count|0
target-employee-count|3
ERROR: permission denied for table staff_payroll_ledger
== повторный apply
Tenant rehome stopped. Target tenant already contains staff data; refusing merge. No connection details or row contents were printed.
exit=1
повтор: БД не изменилась
FK (определения + convalidated), md5 по всем FK: источник 54ee68489eced699e40a343f4ba4134e, копия после переноса 54ee68489eced699e40a343f4ba4134e
## 4. Сценарий [PILOT-REHOME] через диагностический адаптер C6 → c73bb3b (адаптер НЕ в продукте и НЕ в сценарии)
# Вариант 1: адаптер только переводит флаги/переменные/формат вывода.
$ E2E_DATABASE_NAME=krona_diag_rehome_e2e node scripts/test/run-e2e.mjs
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 183ms
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 96ms
→ expected 'undefined' to be '328228' // Object.is equality
# Вариант 2: адаптер дополнительно сам досчитывает SUM(amount_minor) для --dry-run (продукт её там не печатает).
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 177ms
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 460ms
### Исходный код адаптера (вариант 2)
```js
// ДИАГНОСТИЧЕСКИЙ АДАПТЕР (не коммитится в продукт, не часть сценария): переводит контракт C6
// (--from/--to/--dry-run/--json, DATABASE_ADMIN_URL, одна строка JSON с ledger_amount_minor_sum)
// в интерфейс codex/fix-tenant-rehome c73bb3b (--source/--target, --dry-run|--apply, --actor,
// --backup-evidence, DATABASE_REHOME_URL, многострочный JSON). Evidence — синтетическая.
import { spawnSync } from 'node:child_process';
import { writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const a = process.argv.slice(2);
const get = (k) => a[a.indexOf(k) + 1];
const dry = a.includes('--dry-run');
const args = ['--source', get('--from'), '--target', get('--to')];
if (dry) args.unshift('--dry-run');
else {
const dir = mkdtempSync(path.join(tmpdir(), 'rehome-ev-'));
const sha = 'b'.repeat(64);
writeFileSync(
path.join(dir, 'e.json'),
JSON.stringify({
backup: {
format: 'pg_dump -Fc',
encrypted: true,
offHost: true,
sha256: sha,
createdAt: '2026-09-26T10:00:00Z',
},
restoreDrill: {
status: 'passed',
backupSha256: sha,
restoredAt: '2026-09-26T10:01:00Z',
verifiedAt: '2026-09-26T10:02:00Z',
isolatedDatabase: 'diag',
},
}),
);
args.unshift('--apply');
args.push('--actor', '01M3ACT0R000000000000000AA', '--backup-evidence', path.join(dir, 'e.json'));
}
const r = spawnSync(
process.execPath,
[path.join(import.meta.dirname, 'rehome-tenant.product.mjs'), ...args],
{
env: { ...process.env, DATABASE_REHOME_URL: process.env.DATABASE_ADMIN_URL },
encoding: 'utf8',
},
);
process.stderr.write(r.stderr);
if (r.status !== 0) process.exit(r.status ?? 1);
const rep = JSON.parse(r.stdout);
const out = {
tables: Object.fromEntries(Object.entries(rep.tables).map(([k, v]) => [k, Number(v)])),
};
if (rep.payrollLedger)
out.ledger_amount_minor_sum = rep.payrollLedger
.reduce((s, x) => s + BigInt(x.amountMinor), 0n)
.toString();
if (dry && !out.ledger_amount_minor_sum) {
// ВАРИАНТ 2: сумму досчитывает сам адаптер (продукт её в dry-run не печатает), чтобы проверить остальные шаги.
const pg = (await import('pg')).default;
const c = new pg.Client({ connectionString: process.env.DATABASE_ADMIN_URL });
await c.connect();
out.ledger_amount_minor_sum = (
await c.query(
'SELECT coalesce(sum(amount_minor),0)::text s FROM staff_payroll_ledger WHERE tenant_id=$1',
[get('--from')],
)
).rows[0].s;
await c.end();
}
console.log(JSON.stringify(out));
```
### Исходный код drill.sh
```bash
#!/usr/bin/env bash
# K18: backup.sh (codex/fix-pilot-backups a005c5e) → шифрованный файл → проверка sha256 → расшифровка
# → pg_restore в новую БД → restore-drill-report.sql на источнике и копии → сравнение → isolation.
# Docker нет: `docker compose exec -T postgres sh -eu -c '<cmd>'` подменён шимом, который выполняет
# ту же команду pg_dump локально против PG17 (суперпользователь, как POSTGRES_USER в образе postgres).
set -euo pipefail
WT=/workspace/krona-staff-acceptance/diag-pilot-backups
PGBIN=/workspace/.mamba-root/envs/pg17/bin
export PGHOST=127.0.0.1 PGPORT=55433 PGUSER=pgadmin PGPASSWORD="$(cat /workspace/.pg17-secrets/admin.pw)"
psql() { "$PGBIN/psql" -X -v ON_ERROR_STOP=1 "$@"; }
T=$(mktemp -d /tmp/k18-drill.XXXXXX); trap 'rm -rf "$T"' EXIT
mkdir -p "$T/bin" "$T/server" "$T/pc" "$T/offhost-key"
echo "== 1. источник: копия krona_acc_e2e (данные последнего e2e-прогона accept-branch)"
psql -d postgres -q -c "DROP DATABASE IF EXISTS krona_k18_src" -c "DROP DATABASE IF EXISTS krona_k18_restore" \
-c "CREATE DATABASE krona_k18_src TEMPLATE krona_acc_e2e OWNER krona_migrator" \
-c "REVOKE ALL ON DATABASE krona_k18_src FROM PUBLIC" -c "GRANT CONNECT ON DATABASE krona_k18_src TO krona_staff_app"
cat > "$T/bin/docker" <<SHIM
#!/bin/sh
for last; do :; done
exec env PATH="$PGBIN:\$PATH" POSTGRES_USER=pgadmin POSTGRES_DB=krona_k18_src sh -eu -c "\$last"
SHIM
chmod +x "$T/bin/docker"
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 1 -keyout "$T/offhost-key/recipient.key" \
-out "$T/server/recipient.crt" -subj '/CN=k18-drill' >/dev/null 2>&1
echo "== 2. deploy/pilot/backup.sh (external-pull)"
PATH="$T/bin:$PATH" BACKUP_DIRECTORY="$T/server/backups" BACKUP_RECIPIENT_CERT="$T/server/recipient.crt" \
BACKUP_TRANSFER_MODE=external-pull sh "$WT/deploy/pilot/backup.sh" | sed "s#$T#<tmp>#g"
echo "exit=$?"
f=$(ls "$T/server/backups"/*.dump.cms); n=$(basename "$f")
echo "plaintext на сервере: $(find "$T/server/backups" -type f ! -name '*.dump.cms' ! -name '*.dump.cms.sha256' | wc -l) файлов"
head -c 4 "$f" | od -An -tx1 | sed 's/^/первые байты (DER): /'
echo "== 3. «перенос на ПК» (cp вместо ssh/scp — pull-pilot-backups.sh здесь не запускался) + sha256"
cp "$f" "$f.sha256" "$T/pc/" && (cd "$T/pc" && sha256sum -c "$n.sha256")
sha=$(cut -d' ' -f1 "$T/pc/$n.sha256")
echo "== 4. расшифровка ключом вне сервера + pg_restore в новую БД"
openssl cms -decrypt -binary -inform DER -in "$T/pc/$n" -recip "$T/server/recipient.crt" \
-inkey "$T/offhost-key/recipient.key" -out "$T/pc/restore.dump"
"$PGBIN/pg_restore" -l "$T/pc/restore.dump" | grep -c "TABLE DATA" | sed 's/^/TABLE DATA в дампе: /'
psql -d postgres -q -c "CREATE DATABASE krona_k18_restore TEMPLATE template0 OWNER krona_migrator" \
-c "REVOKE ALL ON DATABASE krona_k18_restore FROM PUBLIC" -c "GRANT CONNECT ON DATABASE krona_k18_restore TO krona_staff_app"
"$PGBIN/pg_restore" --exit-on-error -d krona_k18_restore "$T/pc/restore.dump"; echo "pg_restore exit=$?"
echo "== 5. restore-drill-report.sql: источник vs копия"
psql -d krona_k18_src -At -f "$WT/scripts/restore-drill-report.sql" > "$T/src.txt"
psql -d krona_k18_restore -At -f "$WT/scripts/restore-drill-report.sql" > "$T/restore.txt"
echo "строк отчёта: $(wc -l < "$T/src.txt"); diff:"; diff "$T/src.txt" "$T/restore.txt" && echo "идентичны"
grep -E '^(RLS|LEDGER)' "$T/restore.txt" | sed -E 's/[0-9A-HJKMNP-TV-Z]{26}/<ulid>/g'
psql -d krona_k18_restore -At -c "select count(*) from schema_migrations" | sed 's/^/schema_migrations: /'
psql -d krona_k18_restore -At -c "select tablename||' owner='||tableowner from pg_tables where schemaname='public' and tablename='staff_payroll_ledger'"
psql -d krona_k18_restore -At -c "select count(*) from information_schema.role_table_grants where grantee='krona_staff_app'" | sed 's/^/гранты krona_staff_app: /'
echo "== 6. restore-drill-isolation.sql (krona_staff_app, NOBYPASSRLS)"
psql -d krona_k18_restore -At -f "$WT/scripts/restore-drill-isolation.sql"
printf '%s\n' "$sha" > /tmp/ab/k18/backup.sha256
```
### Исходный код rehome-on-copy.sh
```bash
#!/usr/bin/env bash
# TASK-CODEX-2 п.2.2 на восстановленной копии (krona_k18_restore из drill.sh), интерфейс продукта c73bb3b как есть.
set -uo pipefail
WT=/workspace/krona-staff-acceptance/diag-tenant-rehome
BK=/workspace/krona-staff-acceptance/diag-pilot-backups
PGBIN=/workspace/.mamba-root/envs/pg17/bin
export PGHOST=127.0.0.1 PGPORT=55433 PGUSER=pgadmin PGPASSWORD="$(cat /workspace/.pg17-secrets/admin.pw)"
DB=krona_k18_restore
URL="postgres://pgadmin:${PGPASSWORD}@127.0.0.1:55433/$DB"
STUB=01J00000000000000000000000; TARGET=01M2R6AC5652NPT3MG34DKN4ZP
psql() { "$PGBIN/psql" -X -v ON_ERROR_STOP=1 "$@"; }
mask() { sed -E "s/$STUB/<STUB>/g; s/$TARGET/<TARGET>/g; s/[0-9A-HJKMNP-TV-Z]{26}/<other>/g"; }
cd "$WT"
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" > /tmp/ab/k18/before.txt
echo "== dry-run: node scripts/rehome-tenant.mjs --dry-run --source <STUB> --target <TARGET>"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --dry-run --source $STUB --target $TARGET > /tmp/ab/k18/dry.json; echo "exit=$?"
mask < /tmp/ab/k18/dry.json | tr -d '\n ' | cut -c1-900; echo
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" > /tmp/ab/k18/after-dry.txt
diff -q /tmp/ab/k18/before.txt /tmp/ab/k18/after-dry.txt && echo "dry-run: БД не изменилась"
echo "== C6-интерфейс на этом же скрипте"
DATABASE_ADMIN_URL="$URL" node scripts/rehome-tenant.mjs --from $STUB --to $TARGET --dry-run --json; echo "exit=$?"
echo "== apply без --backup-evidence"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --apply --source $STUB --target $TARGET --actor 01M3ACT0R000000000000000AA; echo "exit=$?"
sha=$(cat /tmp/ab/k18/backup.sha256)
cat > /tmp/ab/k18/evidence.json <<JSON
{"backup":{"format":"pg_dump -Fc","encrypted":true,"offHost":true,"sha256":"$sha","createdAt":"2026-09-26T10:10:55Z"},
"restoreDrill":{"status":"passed","backupSha256":"$sha","restoredAt":"2026-09-26T10:11:00Z","verifiedAt":"2026-09-26T10:11:05Z","isolatedDatabase":"$DB"}}
JSON
echo "== apply: --apply --source <STUB> --target <TARGET> --actor <ulid> --backup-evidence evidence.json"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --apply --source $STUB --target $TARGET --actor 01M3ACT0R000000000000000AA --backup-evidence /tmp/ab/k18/evidence.json > /tmp/ab/k18/apply.json; echo "exit=$?"
mask < /tmp/ab/k18/apply.json | tr -d '\n ' | cut -c1-1200; echo
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" > /tmp/ab/k18/after.txt
echo "== сверка (restore-drill-report.sql до/после; счётчики по таблице/арендатору, RLS, ledger)"
echo "-- строки с <STUB> после: $(grep -c "|$STUB|" /tmp/ab/k18/after.txt)"
echo "-- прочие арендаторы до/после идентичны: $(diff <(grep -v -e "|$STUB|" -e "|$TARGET|" /tmp/ab/k18/before.txt) <(grep -v -e "|$STUB|" -e "|$TARGET|" /tmp/ab/k18/after.txt) >/dev/null && echo да || echo НЕТ)"
echo "-- счётчики <STUB> до vs <TARGET> после (кроме audit):"
diff <(grep "|$STUB|" /tmp/ab/k18/before.txt | grep -v '^staff_audit_log' | sed "s/$STUB/X/") <(grep "|$TARGET|" /tmp/ab/k18/after.txt | grep -v '^staff_audit_log' | sed "s/$TARGET/X/") && echo " совпадают"
echo "-- audit: до $(grep "^staff_audit_log|$STUB|" /tmp/ab/k18/before.txt | cut -d'|' -f3), после $(grep "^staff_audit_log|$TARGET|" /tmp/ab/k18/after.txt | cut -d'|' -f3)"
grep "^LEDGER" /tmp/ab/k18/after.txt | mask; grep "^LEDGER" /tmp/ab/k18/before.txt | mask | sed 's/^/до: /'
psql -d $DB -At -c "select action||' '||entity_type from staff_audit_log where tenant_id='$TARGET' and action like 'tenant.%'"
psql -d $DB -At -c "select count(*) from pg_constraint where contype='f' and connamespace='public'::regnamespace" | sed 's/^/FK после: /'
echo "== krona_staff_app"
psql -d $DB -At -f "$BK/scripts/restore-drill-isolation.sql" | grep count
psql -d $DB -At -c "BEGIN; SET LOCAL ROLE krona_staff_app; SET LOCAL app.tenant_id='$TARGET'; UPDATE staff_payroll_ledger SET amount_minor=amount_minor+1;" 2>&1 | tail -1
echo "== повторный apply"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --apply --source $STUB --target $TARGET --actor 01M3ACT0R000000000000000AA --backup-evidence /tmp/ab/k18/evidence.json; echo "exit=$?"
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" | diff -q - /tmp/ab/k18/after.txt && echo "повтор: БД не изменилась"
```