krona-staff/tests/e2e/report-evidence/round3-k18-and-rehome.md
grok 43b684e04f 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] через
диагностический адаптер (не в продукте, не в сценарии) — исходники скриптов приложены.
2026-09-26 17:12:07 +07:00

290 lines
19 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.

# 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 "повтор: БД не изменилась"
```