test(e2e): готовые роли через E2E_MIGRATOR_URL/E2E_APP_URL

Почему: TASK-GROK-3 п.1 — на общем кластере (параллельные прогоны приёмки, чужой стенд)
пересоздание ролей krona_migrator/krona_staff_app со случайными паролями ломает соседей.
Если заданы оба URL, global-setup не вызывает provision и не требует E2E_PG_ADMIN_URL;
по умолчанию поведение прежнее. provision-pg получил --roles keep: пересоздать только БД,
не меняя паролей (нужно accept-branch для отдельной БД на ветку).
ctx.adminDatabaseUrl (E2E_DB_ADMIN_URL или E2E_PG_ADMIN_URL + БД стенда) — только для PILOT-REHOME.
Проверено: run-e2e с E2E_MIGRATOR_URL/E2E_APP_URL без E2E_PG_ADMIN_URL — 45 failed | 9 passed,
как на основе; повторный прогон на уже мигрированной БД — то же.
This commit is contained in:
grok 2026-09-26 16:01:19 +07:00
parent ba59a48ffd
commit aa7f5f8bbd
3 changed files with 82 additions and 19 deletions

View file

@ -14,9 +14,13 @@ for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], proce
const adminUrl = process.env.E2E_PG_ADMIN_URL;
const envFile = args.get('--out');
const database = args.get('--db') ?? 'krona_staff_e2e';
if (!adminUrl || !envFile) {
// --roles keep: роли уже созданы (например, предыдущим вызовом accept-branch) — пароли НЕ меняются,
// только пересоздаётся БД. Нужно, чтобы параллельные/последовательные стенды не ломали друг другу пароли.
const keepRoles = args.get('--roles') === 'keep';
if (!adminUrl || (!envFile && !keepRoles)) {
console.error(
'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out <env-path> [--db name]',
'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out <env-path> [--db name]\n' +
' E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --roles keep --db name',
);
process.exit(2);
}
@ -31,6 +35,13 @@ export async function provision() {
});
await admin.connect();
try {
if (keepRoles) {
const existing = await admin.query(
`SELECT count(*)::int AS n FROM pg_roles WHERE rolname IN ('krona_migrator','krona_staff_app')`,
);
if (existing.rows[0].n !== 2)
throw new Error('--roles keep: роли krona_migrator/krona_staff_app не созданы');
} else {
await admin.query(`
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN
@ -50,6 +61,7 @@ export async function provision() {
[appPassword],
);
await admin.query(alterApp.rows[0].sql);
}
await admin.query(
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,
[database],
@ -73,6 +85,10 @@ export async function provision() {
} finally {
await dbAdmin.end();
}
if (keepRoles) {
console.log(`recreated database=${database} (roles kept, passwords unchanged)`);
return;
}
const base = new URL(adminUrl);
const url = (user, password) => {
const u = new URL(base.toString());

View file

@ -11,6 +11,8 @@ export interface E2EContext {
terminalActorId: string;
appDatabaseUrl: string;
migrationDatabaseUrl: string;
/** Админ (владелец кластера) в БД стенда; null, если не передан. Только для PILOT-REHOME. */
adminDatabaseUrl: string | null;
}
declare module 'vitest' {

View file

@ -103,12 +103,47 @@ async function startServer(name: string, env: Record<string, string>): Promise<S
throw new Error(`server ${name} did not become ready; see ${logPath}`);
}
export default async function setup(project: TestProject) {
mkdirSync(evidenceDir, { recursive: true });
/**
* Откуда брать БД и роли.
* 1. E2E_MIGRATOR_URL и E2E_APP_URL заданы — роли и пароли уже есть (общий кластер,
* параллельная работа других): стенд их не создаёт и не меняет, БД берётся из URL.
* 2. Иначе (по умолчанию) — bootstrap-админ E2E_PG_ADMIN_URL создаёт роли со
* случайными паролями и пересоздаёт БД E2E_DATABASE_NAME (scripts/test/provision-pg.mjs).
* adminDatabaseUrl — админ в той же БД (нужен только сценарию PILOT-REHOME):
* E2E_DB_ADMIN_URL, либо E2E_PG_ADMIN_URL с именем БД стенда.
*/
function resolveDatabase(): {
migrationUrl: string;
appUrl: string;
adminDatabaseUrl: string | null;
} {
const adminUrl = process.env.E2E_PG_ADMIN_URL;
const withDatabase = (url: string, database: string) => {
const result = new URL(url);
result.pathname = `/${database}`;
return result.toString();
};
const presetMigrator = process.env.E2E_MIGRATOR_URL;
const presetApp = process.env.E2E_APP_URL;
if (presetMigrator && presetApp) {
const database = new URL(presetApp).pathname.slice(1);
console.log(
`[e2e] роли заданы E2E_MIGRATOR_URL/E2E_APP_URL: provision пропущен, БД ${database}`,
);
return {
migrationUrl: presetMigrator,
appUrl: presetApp,
adminDatabaseUrl:
process.env.E2E_DB_ADMIN_URL ?? (adminUrl ? withDatabase(adminUrl, database) : null),
};
}
if (presetMigrator || presetApp) {
throw new Error('E2E_MIGRATOR_URL и E2E_APP_URL задаются только вместе');
}
if (!adminUrl) {
throw new Error(
'E2E_PG_ADMIN_URL не задан: нужен bootstrap-админ PostgreSQL 17 (только для создания ролей и БД)',
'E2E_PG_ADMIN_URL не задан: нужен bootstrap-админ PostgreSQL 17 (только для создания ролей и БД) ' +
'или готовые роли в E2E_MIGRATOR_URL + E2E_APP_URL',
);
}
const database = process.env.E2E_DATABASE_NAME ?? 'krona_staff_e2e';
@ -121,10 +156,19 @@ export default async function setup(project: TestProject) {
);
const dbEnv = parseEnvFile(envFile);
rmSync(secretsDir, { recursive: true, force: true });
const migrationUrl = dbEnv.DATABASE_MIGRATION_URL;
const appUrl = dbEnv.DATABASE_URL;
if (!migrationUrl || !appUrl) throw new Error('provision did not produce connection URLs');
if (!dbEnv.DATABASE_MIGRATION_URL || !dbEnv.DATABASE_URL) {
throw new Error('provision did not produce connection URLs');
}
return {
migrationUrl: dbEnv.DATABASE_MIGRATION_URL,
appUrl: dbEnv.DATABASE_URL,
adminDatabaseUrl: process.env.E2E_DB_ADMIN_URL ?? withDatabase(adminUrl, database),
};
}
export default async function setup(project: TestProject) {
mkdirSync(evidenceDir, { recursive: true });
const { migrationUrl, appUrl, adminDatabaseUrl } = resolveDatabase();
const migrationMode = process.env.E2E_MIGRATIONS ?? 'up';
runNode(
['scripts/test/migrate-roundtrip.mjs', migrationMode === 'roundtrip' ? 'roundtrip' : 'up'],
@ -176,6 +220,7 @@ export default async function setup(project: TestProject) {
terminalActorId,
appDatabaseUrl: appUrl,
migrationDatabaseUrl: migrationUrl,
adminDatabaseUrl,
};
project.provide('e2e', context);