diff --git a/tests/e2e/report-evidence/round3-accept-f7d3c73.md b/tests/e2e/report-evidence/round3-accept-f7d3c73.md new file mode 100644 index 0000000..4e23084 --- /dev/null +++ b/tests/e2e/report-evidence/round3-accept-f7d3c73.md @@ -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 — ТОЛЬКО ФОРМАТИРОВАНИЕ diff --git a/tests/e2e/report-evidence/round3-k18-and-rehome.md b/tests/e2e/report-evidence/round3-k18-and-rehome.md new file mode 100644 index 0000000..7745e0f --- /dev/null +++ b/tests/e2e/report-evidence/round3-k18-and-rehome.md @@ -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 ' выполняет локально + +# (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=/server/backups/krona-staff-20260926T101055Z-1837466.dump.cms +checksum=/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||RUB|263568 +LEDGER||RUB|22200 +LEDGER||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 --target +exit=0 +{"mode":"dry-run","source":"","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 --target --actor --backup-evidence evidence.json +exit=0 +{"mode":"apply","source":"","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) +-- строки с после: 0 +-- прочие арендаторы до/после идентичны: да +-- счётчики до vs после (кроме audit): +совпадают +-- audit: до 2, после 3 +LEDGER||RUB|263568 +LEDGER||RUB|22200 +LEDGER||RUB|329705 +до: LEDGER||RUB|263568 +до: LEDGER||RUB|22200 +до: LEDGER||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 ''` подменён шимом, который выполняет +# ту же команду 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" </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##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}//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//g; s/$TARGET//g; s/[0-9A-HJKMNP-TV-Z]{26}//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 --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 < --target --actor --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 "-- строки с после: $(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 "-- счётчики до vs после (кроме 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 "повтор: БД не изменилась" +```