From 6f408bc0aa218f5a8f765f57a554a8a70797cd9c Mon Sep 17 00:00:00 2001 From: grok Date: Sat, 26 Sep 2026 16:16:23 +0700 Subject: [PATCH] =?UTF-8?q?test(accept):=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=91=D0=94=20=D0=B8=20=D0=B2=D0=B5=D1=80=D0=B4?= =?UTF-8?q?=D0=B8=D0=BA=D1=82=20SKIP=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Почему: live-postgres-remediation.test.ts (codex/fix-live-database) запускается только при DATABASE_TEST_ISOLATED=true, DATABASE_TEST_GUARD_TOKEN, таблице krona_staff_test_guard и имени БД krona_staff_audit_*; иначе весь файл пропускается, и первый прогон показал его как FAIL «1 skipped (1)», что неверно в обе стороны. В admin-режиме интеграционная БД одноразовая (krona_staff_audit_acc_it), поэтому предохранитель создаётся со случайным маркером — тест реально выполнился: 1 passed. В режиме E2E_MIGRATOR_URL/E2E_APP_URL изоляция не гарантирована, предохранитель не ставится, файл получает отдельный вердикт SKIP (ничего не проверил). --- scripts/test/accept-branch.mjs | 42 ++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/scripts/test/accept-branch.mjs b/scripts/test/accept-branch.mjs index 6dc935b..5a22339 100644 --- a/scripts/test/accept-branch.mjs +++ b/scripts/test/accept-branch.mjs @@ -19,7 +19,11 @@ // Итог — markdown в stdout (и в --out). Логи — в tests/e2e/evidence/accept/<время>/. // // БД: по умолчанию E2E_PG_ADMIN_URL (bootstrap-админ) один раз создаёт роли со случайными -// паролями, дальше только пересоздаёт БД `krona_acc_it`/`krona_acc_e2e` (provision-pg --roles keep). +// паролями, дальше только пересоздаёт БД `krona_staff_audit_acc_it`/`krona_acc_e2e` (provision-pg --roles keep). +// Интеграционная БД в этом режиме заведомо изолирована, поэтому для тестов с «предохранителем» +// (codex/fix-live-database: DATABASE_TEST_ISOLATED, DATABASE_TEST_GUARD_TOKEN, таблица +// krona_staff_test_guard, имя БД krona_staff_audit_*) он выставляется со случайным маркером. +// Файл, где все тесты пропущены, — отдельный вердикт SKIP (ничего не проверил). // Если заданы E2E_MIGRATOR_URL и E2E_APP_URL — роли не создаются и пароли не меняются, // все прогоны идут последовательно в БД из этих URL со сбросом через откат всех миграций. // @@ -28,7 +32,7 @@ // --skip-integration, --skip-e2e, --out <файл.md>. // Ветка задаётся как `origin/<имя>`, `<имя>` (ищется origin/<имя>, затем локальный ref) или SHA. import { spawn, spawnSync } from 'node:child_process'; -import { createHash } from 'node:crypto'; +import { createHash, randomBytes } from 'node:crypto'; import { cpSync, existsSync, @@ -42,6 +46,7 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import pg from 'pg'; import * as prettier from 'prettier'; const repoRoot = path.resolve(import.meta.dirname, '../..'); @@ -296,6 +301,23 @@ async function migrate(wt, db, logFile) { return r; } +/** Предохранитель изолированной тестовой БД (соглашение теста codex/fix-live-database). */ +async function createGuard(db) { + const marker = randomBytes(16).toString('hex'); + const client = new pg.Client({ connectionString: db.migrator }); + await client.connect(); + try { + await client.query( + 'CREATE TABLE IF NOT EXISTS krona_staff_test_guard (marker text PRIMARY KEY)', + ); + await client.query('INSERT INTO krona_staff_test_guard (marker) VALUES ($1)', [marker]); + await client.query('GRANT SELECT ON krona_staff_test_guard TO krona_staff_app'); + } finally { + await client.end(); + } + return marker; +} + async function stepIntegration(target, wt) { const dir = path.join(wt.dir, 'tests/integration'); const files = existsSync(dir) @@ -307,8 +329,9 @@ async function stepIntegration(target, wt) { for (const file of files) { progress(`${target.name}: integration ${file}`); const slug = `${slugOf(target.name)}-it-${file.replace(/\.test\.ts$/, '')}`; - const db = await freshDatabase('krona_acc_it'); + const db = await freshDatabase('krona_staff_audit_acc_it'); const m = await migrate(wt, db, path.join(logDir, `${slug}-migrate.txt`)); + const guard = m.code === 0 && dbMode === 'admin' ? await createGuard(db) : null; if (m.code !== 0) { results.push({ file, @@ -328,6 +351,7 @@ async function stepIntegration(target, wt) { DATABASE_TEST_URL: db.app, DATABASE_TEST_MIGRATION_URL: db.migrator, DATABASE_MIGRATION_URL: db.migrator, + ...(guard ? { DATABASE_TEST_ISOLATED: 'true', DATABASE_TEST_GUARD_TOKEN: guard } : {}), }), logFile: path.join(logDir, `${slug}.txt`), }, @@ -335,12 +359,14 @@ async function stepIntegration(target, wt) { const filesLine = /Test Files\s+(.+)$/m.exec(r.out)?.[1]?.trim() ?? '(нет строки Test Files)'; const testsLine = /^\s*Tests\s+(.+)$/m.exec(r.out)?.[1]?.trim() ?? ''; const ok = r.code === 0 && /\bpassed\b/.test(filesLine) && !/failed/.test(filesLine); + const skipped = r.code === 0 && /skipped/.test(filesLine) && !/passed|failed/.test(filesLine); results.push({ file, ok, + skipped, files: filesLine, tests: testsLine, - error: ok ? '' : firstError(r.out), + error: ok || skipped ? '' : firstError(r.out), }); } return results; @@ -574,9 +600,11 @@ if (needRuns) { ? `OK (${res.check.tests})` : `FAIL exit=${res.check.code}: ${res.check.error}` : 'не запускался'; - const reds = res.integration?.filter((r) => !r.ok).map((r) => r.file) ?? []; + const reds = res.integration?.filter((r) => !r.ok && !r.skipped).map((r) => r.file) ?? []; + const skips = res.integration?.filter((r) => r.skipped).map((r) => r.file) ?? []; + const greens = (res.integration?.length ?? 0) - reds.length - skips.length; const integration = res.integration - ? `${res.integration.length - reds.length}/${res.integration.length} зелёных${reds.length ? `; FAIL: ${reds.join(', ')}` : ''}` + ? `${greens}/${res.integration.length} зелёных${reds.length ? `; FAIL: ${reds.join(', ')}` : ''}${skips.length ? `; SKIP (файл пропущен целиком): ${skips.join(', ')}` : ''}` : 'не запускались'; let e2e = 'не запускался'; let green = ''; @@ -609,7 +637,7 @@ if (needRuns) { for (const res of results.values()) { for (const r of res.integration ?? []) { md.push( - `| ${res.target.ref} | ${r.file} | ${r.ok ? 'OK' : 'FAIL'} | ${r.files} | ${r.tests} | ${r.error} |`, + `| ${res.target.ref} | ${r.file} | ${r.ok ? 'OK' : r.skipped ? 'SKIP' : 'FAIL'} | ${r.files} | ${r.tests} | ${r.error} |`, ); } }