Compare commits
No commits in common. "986843d8d429901d4d8a15a854ec462a46fb040f" and "ba59a48ffd6a6db6d9ad66f2b20a15aa5d5abb90" have entirely different histories.
986843d8d4
...
ba59a48ffd
17 changed files with 28 additions and 1938 deletions
|
|
@ -1,697 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// Приёмка веток исполнителей одной командой (TASK-GROK-3 п.1–2).
|
||||
//
|
||||
// node scripts/test/accept-branch.mjs [опции] <ветка> [<ветка>…]
|
||||
//
|
||||
// Для основы (--base) и каждой ветки:
|
||||
// 1. git worktree в $ACCEPT_ROOT (по умолчанию ../krona-staff-acceptance), detached HEAD;
|
||||
// node_modules — symlink на текущий репозиторий, если package-lock.json совпадает, иначе npm ci;
|
||||
// 2. оверлей ТЕКУЩЕГО стенда: tests/e2e/** и scripts/test/** из этого checkout
|
||||
// (tests/integration, src, миграции — версии ветки, не трогаются);
|
||||
// 3. `npm run check` без переменных БД;
|
||||
// 4. КАЖДЫЙ tests/integration/*.test.ts отдельно на свежей мигрированной БД PostgreSQL;
|
||||
// красный — по строке `Test Files` (ловит ошибки beforeAll/afterAll, при которых
|
||||
// `Tests N passed`), плюс ненулевой exit;
|
||||
// 5. e2e (scripts/test/run-e2e.mjs, миграции roundtrip) и сравнение с основой по ID сценария;
|
||||
// 6. попарно `git merge-tree --write-tree` — все ветки между собой и с основой;
|
||||
// 7. «форматирование без поведения» для веток с `format` в имени и для --format-check <ветка>:
|
||||
// версия файла из merge-base → prettier (конфиг основы) → сравнение с версией ветки.
|
||||
// Итог — markdown в stdout (и в --out). Логи — в tests/e2e/evidence/accept/<время>/.
|
||||
//
|
||||
// БД: по умолчанию E2E_PG_ADMIN_URL (bootstrap-админ) один раз создаёт роли со случайными
|
||||
// паролями, дальше только пересоздаёт БД `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 со сбросом через откат всех миграций.
|
||||
//
|
||||
// Опции: --base <ref> (origin/codex/bootstrap-platform-foundation), --format-check <ref>
|
||||
// (повторяемая; ветка проверяется только на формат, без worktree), --skip-check,
|
||||
// --skip-integration, --skip-e2e, --out <файл.md>.
|
||||
// Ветка задаётся как `origin/<имя>`, `<имя>` (ищется origin/<имя>, затем локальный ref) или SHA.
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} 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, '../..');
|
||||
const acceptRoot = path.resolve(
|
||||
process.env.ACCEPT_ROOT ?? path.join(repoRoot, '..', 'krona-staff-acceptance'),
|
||||
);
|
||||
|
||||
// ---------- аргументы ----------
|
||||
const argv = process.argv.slice(2);
|
||||
const opts = {
|
||||
base: 'origin/codex/bootstrap-platform-foundation',
|
||||
formatOnly: [],
|
||||
skipCheck: false,
|
||||
skipIntegration: false,
|
||||
skipE2e: false,
|
||||
out: null,
|
||||
branches: [],
|
||||
};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--base') opts.base = argv[++i];
|
||||
else if (arg === '--format-check') opts.formatOnly.push(argv[++i]);
|
||||
else if (arg === '--skip-check') opts.skipCheck = true;
|
||||
else if (arg === '--skip-integration') opts.skipIntegration = true;
|
||||
else if (arg === '--skip-e2e') opts.skipE2e = true;
|
||||
else if (arg === '--out') opts.out = argv[++i];
|
||||
else if (arg === '-h' || arg === '--help') {
|
||||
console.log(readFileSync(new URL(import.meta.url), 'utf8').split('\nimport ')[0]);
|
||||
process.exit(0);
|
||||
} else if (arg.startsWith('--')) {
|
||||
console.error(`неизвестная опция ${arg}`);
|
||||
process.exit(2);
|
||||
} else opts.branches.push(arg);
|
||||
}
|
||||
if (opts.branches.length === 0 && opts.formatOnly.length === 0) {
|
||||
console.error('Usage: node scripts/test/accept-branch.mjs [--base ref] <branch> [<branch>…]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// ---------- утилиты ----------
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const logDir = path.join(repoRoot, 'tests/e2e/evidence/accept', stamp);
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
const progress = (msg) => process.stderr.write(`[accept] ${msg}\n`);
|
||||
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
|
||||
function git(args, { cwd = repoRoot, allowFail = false } = {}) {
|
||||
const r = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
||||
if (r.status !== 0 && !allowFail) {
|
||||
throw new Error(`git ${args.join(' ')} → exit ${r.status}: ${r.stderr}`);
|
||||
}
|
||||
return { code: r.status, out: r.stdout, err: r.stderr };
|
||||
}
|
||||
|
||||
function run(command, args, { cwd, env, logFile, timeoutMs = 20 * 60_000 }) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(command, args, { cwd, env });
|
||||
let out = '';
|
||||
const timer = setTimeout(() => {
|
||||
out += `\n[accept] TIMEOUT ${timeoutMs} ms — процесс убит\n`;
|
||||
child.kill('SIGKILL');
|
||||
}, timeoutMs);
|
||||
for (const stream of [child.stdout, child.stderr]) {
|
||||
stream.on('data', (chunk) => {
|
||||
out += chunk.toString();
|
||||
});
|
||||
}
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
const clean = stripAnsi(out);
|
||||
if (logFile) writeFileSync(logFile, `$ ${[command, ...args].join(' ')}\n${redact(clean)}`);
|
||||
resolve({ code: code ?? 1, out: clean });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Пароли в URL не должны попасть в логи/отчёт. */
|
||||
function redact(text) {
|
||||
return text.replace(/(postgres(?:ql)?:\/\/[^:\s/]+:)[^@\s]+@/g, '$1***@');
|
||||
}
|
||||
|
||||
/** Окружение без переменных БД и секретов стенда. */
|
||||
function cleanEnv(extra = {}) {
|
||||
const env = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (/^(DATABASE_|E2E_|TEST_DATABASE|POSTGRES_|KRONA_)/.test(key)) continue;
|
||||
env[key] = value;
|
||||
}
|
||||
return { ...env, NO_COLOR: '1', FORCE_COLOR: '0', ...extra };
|
||||
}
|
||||
|
||||
function resolveRef(name) {
|
||||
const candidates = name.startsWith('origin/') ? [name] : [`origin/${name}`, name];
|
||||
for (const candidate of candidates) {
|
||||
const r = git(['rev-parse', '--verify', '--quiet', `${candidate}^{commit}`], {
|
||||
allowFail: true,
|
||||
});
|
||||
if (r.code === 0) return { name, ref: candidate, sha: r.out.trim() };
|
||||
}
|
||||
throw new Error(`ветка ${name} не найдена (ни origin/${name}, ни локально)`);
|
||||
}
|
||||
|
||||
const slugOf = (name) =>
|
||||
name
|
||||
.replace(/^origin\//, '')
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
||||
.slice(0, 80);
|
||||
const short = (sha) => sha.slice(0, 7);
|
||||
|
||||
function firstError(text) {
|
||||
const lines = text.split('\n');
|
||||
const patterns = [
|
||||
/((?:[A-Za-z]+Error|error): .+)$/,
|
||||
/(AssertionError.+)$/,
|
||||
/(Error: .+)$/,
|
||||
/(FAIL .+)$/,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
for (const line of lines) {
|
||||
const m = pattern.exec(line.trim());
|
||||
if (m) return m[1].replace(/\|/g, '\\|').slice(0, 180);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ---------- БД ----------
|
||||
const presetMigrator = process.env.E2E_MIGRATOR_URL;
|
||||
const presetApp = process.env.E2E_APP_URL;
|
||||
const adminUrl = process.env.E2E_PG_ADMIN_URL;
|
||||
const dbMode = presetMigrator && presetApp ? 'preset' : 'admin';
|
||||
let roleUrls = null;
|
||||
|
||||
function withDatabase(url, database) {
|
||||
const u = new URL(url);
|
||||
u.pathname = `/${database}`;
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
async function prepareRoles() {
|
||||
if (dbMode === 'preset') {
|
||||
roleUrls = { migrator: presetMigrator, app: presetApp };
|
||||
return 'роли из E2E_MIGRATOR_URL/E2E_APP_URL (не пересоздавались)';
|
||||
}
|
||||
if (!adminUrl) {
|
||||
throw new Error('нужен E2E_PG_ADMIN_URL или пара E2E_MIGRATOR_URL + E2E_APP_URL');
|
||||
}
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'krona-accept-'));
|
||||
const envFile = path.join(dir, 'db.env');
|
||||
const r = await run(
|
||||
process.execPath,
|
||||
['scripts/test/provision-pg.mjs', '--out', envFile, '--db', 'krona_acc_roles'],
|
||||
{ cwd: repoRoot, env: { ...cleanEnv(), E2E_PG_ADMIN_URL: adminUrl } },
|
||||
);
|
||||
if (r.code !== 0) throw new Error(`provision: ${r.out}`);
|
||||
const env = Object.fromEntries(
|
||||
readFileSync(envFile, 'utf8')
|
||||
.split('\n')
|
||||
.filter((l) => l.includes('='))
|
||||
.map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]),
|
||||
);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
roleUrls = { migrator: env.DATABASE_MIGRATION_URL, app: env.DATABASE_URL };
|
||||
return 'роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep';
|
||||
}
|
||||
|
||||
/** Чистая БД: в admin-режиме пересоздаётся, в preset — одна и та же (сброс откатом миграций). */
|
||||
async function freshDatabase(name) {
|
||||
if (dbMode === 'preset') return { ...roleUrls, reset: 'roundtrip' };
|
||||
const r = await run(
|
||||
process.execPath,
|
||||
['scripts/test/provision-pg.mjs', '--roles', 'keep', '--db', name],
|
||||
{ cwd: repoRoot, env: { ...cleanEnv(), E2E_PG_ADMIN_URL: adminUrl } },
|
||||
);
|
||||
if (r.code !== 0) throw new Error(`provision --roles keep ${name}: ${r.out}`);
|
||||
return {
|
||||
migrator: withDatabase(roleUrls.migrator, name),
|
||||
app: withDatabase(roleUrls.app, name),
|
||||
admin: withDatabase(adminUrl, name),
|
||||
reset: 'up',
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- worktree ----------
|
||||
const repoLockHash = createHash('sha256')
|
||||
.update(readFileSync(path.join(repoRoot, 'package-lock.json')))
|
||||
.digest('hex');
|
||||
|
||||
async function prepareWorktree(target) {
|
||||
const dir = path.join(acceptRoot, `wt-${slugOf(target.name)}`);
|
||||
mkdirSync(acceptRoot, { recursive: true });
|
||||
const registered = git(['worktree', 'list', '--porcelain'])
|
||||
.out.split('\n')
|
||||
.some((l) => l === `worktree ${dir}`);
|
||||
if (registered) git(['worktree', 'remove', '--force', dir]);
|
||||
else if (existsSync(dir)) throw new Error(`${dir} существует, но не является worktree`);
|
||||
git(['worktree', 'add', '--detach', '--force', dir, target.sha]);
|
||||
const lock = git(['show', `${target.sha}:package-lock.json`]).out;
|
||||
const lockSame = createHash('sha256').update(lock).digest('hex') === repoLockHash;
|
||||
if (lockSame) {
|
||||
symlinkSync(path.join(repoRoot, 'node_modules'), path.join(dir, 'node_modules'), 'dir');
|
||||
} else {
|
||||
progress(`${target.name}: package-lock.json отличается — npm ci`);
|
||||
const r = await run('npm', ['ci', '--no-audit', '--no-fund'], {
|
||||
cwd: dir,
|
||||
env: cleanEnv(),
|
||||
logFile: path.join(logDir, `${slugOf(target.name)}-npm-ci.txt`),
|
||||
});
|
||||
if (r.code !== 0) throw new Error(`${target.name}: npm ci exit ${r.code}`);
|
||||
}
|
||||
// Оверлей стенда: tests/e2e/** (без evidence) и scripts/test/**.
|
||||
const overlayTouched = git([
|
||||
'diff',
|
||||
'--name-only',
|
||||
`${opts.baseTarget.sha}...${target.sha}`,
|
||||
'--',
|
||||
'tests/e2e',
|
||||
'scripts/test',
|
||||
]).out.trim();
|
||||
for (const rel of ['tests/e2e', 'scripts/test']) {
|
||||
rmSync(path.join(dir, rel), { recursive: true, force: true });
|
||||
cpSync(path.join(repoRoot, rel), path.join(dir, rel), {
|
||||
recursive: true,
|
||||
filter: (src) => !src.includes(`${path.sep}tests${path.sep}e2e${path.sep}evidence`),
|
||||
});
|
||||
}
|
||||
return {
|
||||
dir,
|
||||
deps: lockSame ? 'symlink' : 'npm ci',
|
||||
overlayTouched: overlayTouched ? overlayTouched.split('\n') : [],
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- шаги ----------
|
||||
async function stepCheck(target, wt) {
|
||||
progress(`${target.name}: npm run check`);
|
||||
const r = await run('npm', ['run', 'check'], {
|
||||
cwd: wt.dir,
|
||||
env: cleanEnv(),
|
||||
logFile: path.join(logDir, `${slugOf(target.name)}-check.txt`),
|
||||
});
|
||||
const tests = /Tests\s+(.+)$/m.exec(r.out)?.[1]?.trim() ?? '';
|
||||
return { ok: r.code === 0, code: r.code, tests, error: r.code === 0 ? '' : firstError(r.out) };
|
||||
}
|
||||
|
||||
async function migrate(wt, db, logFile) {
|
||||
const r = await run(process.execPath, ['scripts/test/migrate-roundtrip.mjs', db.reset], {
|
||||
cwd: wt.dir,
|
||||
env: cleanEnv({ DATABASE_MIGRATION_URL: db.migrator }),
|
||||
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)
|
||||
? readdirSync(dir)
|
||||
.filter((f) => f.endsWith('.test.ts'))
|
||||
.sort()
|
||||
: [];
|
||||
const results = [];
|
||||
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_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,
|
||||
ok: false,
|
||||
files: '',
|
||||
tests: '',
|
||||
error: `миграции: ${firstError(m.out)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const r = await run(
|
||||
process.execPath,
|
||||
[path.join(wt.dir, 'node_modules/vitest/vitest.mjs'), 'run', `tests/integration/${file}`],
|
||||
{
|
||||
cwd: wt.dir,
|
||||
env: cleanEnv({
|
||||
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`),
|
||||
},
|
||||
);
|
||||
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 || skipped ? '' : firstError(r.out),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function loadE2e(file) {
|
||||
const map = new Map();
|
||||
for (const f of JSON.parse(readFileSync(file, 'utf8')).testResults) {
|
||||
for (const t of f.assertionResults) {
|
||||
const id = /^\[([A-Z0-9/-]+)\]/.exec(t.title)?.[1] ?? 'UNTAGGED';
|
||||
const e = map.get(id) ?? { pass: 0, fail: 0, msg: '' };
|
||||
if (t.status === 'passed') e.pass += 1;
|
||||
else if (t.status === 'failed') {
|
||||
e.fail += 1;
|
||||
e.msg ||= stripAnsi(t.failureMessages?.[0] ?? '')
|
||||
.split('\n')[0]
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\|/g, '\\|')
|
||||
.slice(0, 160);
|
||||
}
|
||||
map.set(id, e);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function stepE2e(target, wt) {
|
||||
progress(`${target.name}: e2e`);
|
||||
const db = await freshDatabase('krona_acc_e2e');
|
||||
const slug = slugOf(target.name);
|
||||
const env = cleanEnv({
|
||||
E2E_MIGRATOR_URL: db.migrator,
|
||||
E2E_APP_URL: db.app,
|
||||
E2E_MIGRATIONS: 'roundtrip',
|
||||
...(db.admin ? { E2E_DB_ADMIN_URL: db.admin } : {}),
|
||||
});
|
||||
const r = await run(process.execPath, ['scripts/test/run-e2e.mjs'], {
|
||||
cwd: wt.dir,
|
||||
env,
|
||||
logFile: path.join(logDir, `${slug}-e2e.txt`),
|
||||
});
|
||||
const jsonPath = path.join(wt.dir, 'tests/e2e/evidence/e2e-results.json');
|
||||
if (!existsSync(jsonPath))
|
||||
return { ok: false, code: r.code, map: null, error: firstError(r.out) };
|
||||
cpSync(jsonPath, path.join(logDir, `${slug}-e2e-results.json`));
|
||||
const map = loadE2e(jsonPath);
|
||||
const red = [...map.values()].filter((e) => e.fail > 0).length;
|
||||
const verdict = /\[run-e2e\] (OK|FAIL)[^\n]*/.exec(r.out)?.[0] ?? '';
|
||||
return { ok: r.code === 0, code: r.code, map, red, green: map.size - red, verdict };
|
||||
}
|
||||
|
||||
function diffE2e(baseMap, map) {
|
||||
const color = (e) => (e ? (e.fail ? 'RED' : 'GREEN') : 'нет');
|
||||
const turnedGreen = [];
|
||||
const turnedRed = [];
|
||||
const other = [];
|
||||
for (const id of [...new Set([...baseMap.keys(), ...map.keys()])].sort()) {
|
||||
const a = color(baseMap.get(id));
|
||||
const b = color(map.get(id));
|
||||
if (a === b) continue;
|
||||
if (a === 'RED' && b === 'GREEN') turnedGreen.push(id);
|
||||
else if (a === 'GREEN' && b === 'RED') turnedRed.push(`${id} (${map.get(id).msg})`);
|
||||
else other.push(`${id}: ${a}→${b}`);
|
||||
}
|
||||
return { turnedGreen, turnedRed, other };
|
||||
}
|
||||
|
||||
function mergeTree(a, b) {
|
||||
const r = git(['merge-tree', '--write-tree', '--name-only', '--no-messages', a.sha, b.sha], {
|
||||
allowFail: true,
|
||||
});
|
||||
if (r.code === 0) return { status: 'чисто', files: [] };
|
||||
if (r.code === 1) {
|
||||
const files = r.out.split('\n').slice(1).filter(Boolean);
|
||||
return { status: 'КОНФЛИКТ', files };
|
||||
}
|
||||
return { status: `ошибка git (exit ${r.code})`, files: [r.err.trim()] };
|
||||
}
|
||||
|
||||
// ---------- форматирование без поведения ----------
|
||||
async function prettierConfigFor(file) {
|
||||
const anchor = path.join(opts.baseWorktree ?? repoRoot, file);
|
||||
const config = (await prettier.resolveConfig(anchor)) ?? {};
|
||||
const info = await prettier.getFileInfo(anchor, {
|
||||
ignorePath: path.join(opts.baseWorktree ?? repoRoot, '.prettierignore'),
|
||||
});
|
||||
return { config, info };
|
||||
}
|
||||
|
||||
async function normalize(content, file) {
|
||||
const { config, info } = await prettierConfigFor(file);
|
||||
if (info.ignored || !info.inferredParser) return content;
|
||||
return prettier.format(content, { ...config, filepath: file });
|
||||
}
|
||||
|
||||
function showFile(sha, file) {
|
||||
const r = git(['show', `${sha}:${file}`], { allowFail: true });
|
||||
return r.code === 0 ? r.out : null;
|
||||
}
|
||||
|
||||
function changedLines(before, after) {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'krona-fmt-'));
|
||||
writeFileSync(path.join(dir, 'a'), before);
|
||||
writeFileSync(path.join(dir, 'b'), after);
|
||||
const r = git(['diff', '--no-index', '-U0', '--no-color', 'a', 'b'], {
|
||||
cwd: dir,
|
||||
allowFail: true,
|
||||
});
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
return r.out
|
||||
.split('\n')
|
||||
.filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---) /.test(l))
|
||||
.map((l) => `${l[0]}${l.slice(1).trim()}`)
|
||||
.filter((l) => l.length > 1);
|
||||
}
|
||||
|
||||
async function formatCheck(target, others) {
|
||||
const mergeBase = git(['merge-base', opts.baseTarget.sha, target.sha]).out.trim();
|
||||
const changes = git(['diff', '--name-status', '--no-renames', mergeBase, target.sha])
|
||||
.out.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((l) => ({ status: l[0], file: l.split('\t')[1] }));
|
||||
const rows = [];
|
||||
for (const { status, file } of changes) {
|
||||
const touchesConfig = /(^|\/)\.prettier(rc|ignore)|prettier\.config\./.test(file);
|
||||
if (status === 'D') {
|
||||
rows.push({ file, verdict: 'удалён', lines: 0, same: [] });
|
||||
continue;
|
||||
}
|
||||
const after = showFile(target.sha, file);
|
||||
const before = status === 'A' ? null : showFile(mergeBase, file);
|
||||
if (before === null) {
|
||||
rows.push({ file, verdict: 'новый файл', lines: 0, same: [] });
|
||||
continue;
|
||||
}
|
||||
const formatted = await normalize(before, file);
|
||||
if (formatted === after && !touchesConfig) continue; // чистое форматирование
|
||||
const lines = changedLines(formatted, after);
|
||||
const same = [];
|
||||
for (const other of others) {
|
||||
const otherContent = showFile(other.sha, file);
|
||||
if (otherContent === null) continue;
|
||||
const otherMergeBase = git(['merge-base', opts.baseTarget.sha, other.sha]).out.trim();
|
||||
const otherBase = showFile(otherMergeBase, file) ?? '';
|
||||
const otherLines = new Set(
|
||||
changedLines(await normalize(otherBase, file), await normalize(otherContent, file)),
|
||||
);
|
||||
const matched = lines.filter((l) => otherLines.has(l)).length;
|
||||
if (matched > 0) same.push(`${other.name}: ${matched}/${lines.length}`);
|
||||
}
|
||||
rows.push({
|
||||
file,
|
||||
verdict: touchesConfig ? 'меняет конфиг prettier' : 'не совпадает с prettier(база)',
|
||||
lines: lines.length,
|
||||
same,
|
||||
});
|
||||
}
|
||||
return { mergeBase, files: changes.length, rows };
|
||||
}
|
||||
|
||||
// ---------- main ----------
|
||||
opts.baseTarget = resolveRef(opts.base);
|
||||
const targets = opts.branches.map(resolveRef);
|
||||
const formatTargets = [
|
||||
...targets.filter((t) => /format/i.test(t.name)),
|
||||
...opts.formatOnly.map(resolveRef),
|
||||
];
|
||||
const harnessHead = git(['rev-parse', '--short', 'HEAD']).out.trim();
|
||||
const harnessDirty = git(['status', '--porcelain', '--', 'tests/e2e', 'scripts/test']).out.trim()
|
||||
? ' (+ незакоммиченные изменения стенда)'
|
||||
: '';
|
||||
|
||||
const needRuns = !(opts.skipCheck && opts.skipIntegration && opts.skipE2e) && targets.length > 0;
|
||||
let dbNote = 'не использовалась';
|
||||
if (needRuns && !(opts.skipIntegration && opts.skipE2e)) dbNote = await prepareRoles();
|
||||
|
||||
const results = new Map();
|
||||
const all = [opts.baseTarget, ...targets];
|
||||
for (const target of all) {
|
||||
const res = { target };
|
||||
results.set(target, res);
|
||||
if (!needRuns && target !== opts.baseTarget) continue;
|
||||
try {
|
||||
const wt = await prepareWorktree(target);
|
||||
res.wt = wt;
|
||||
if (target === opts.baseTarget) opts.baseWorktree = wt.dir;
|
||||
if (!needRuns) continue;
|
||||
if (!opts.skipCheck) res.check = await stepCheck(target, wt);
|
||||
if (!opts.skipIntegration) res.integration = await stepIntegration(target, wt);
|
||||
if (!opts.skipE2e) res.e2e = await stepE2e(target, wt);
|
||||
} catch (error) {
|
||||
res.fatal = redact(String(error?.message ?? error)).slice(0, 300);
|
||||
progress(`${target.name}: ${res.fatal}`);
|
||||
}
|
||||
}
|
||||
|
||||
const formatResults = [];
|
||||
for (const target of formatTargets) {
|
||||
progress(`${target.name}: проверка форматирования`);
|
||||
const others = targets.filter((t) => t !== target && !/format/i.test(t.name));
|
||||
formatResults.push({ target, ...(await formatCheck(target, others)) });
|
||||
}
|
||||
|
||||
// ---------- markdown ----------
|
||||
const md = [];
|
||||
const baseRes = results.get(opts.baseTarget);
|
||||
md.push(`## accept-branch — ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC`);
|
||||
md.push('');
|
||||
md.push(
|
||||
`База: \`${opts.baseTarget.ref}\` = ${short(opts.baseTarget.sha)}; стенд (оверлей tests/e2e, scripts/test): ${harnessHead}${harnessDirty}; БД: ${dbNote}. Логи: \`${path.relative(repoRoot, logDir)}/\`.`,
|
||||
);
|
||||
md.push('');
|
||||
if (needRuns) {
|
||||
md.push('### Сводка');
|
||||
md.push('');
|
||||
md.push(
|
||||
'| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |',
|
||||
);
|
||||
md.push('|---|---|---|---|---|---|---|');
|
||||
for (const res of results.values()) {
|
||||
const t = res.target;
|
||||
const isBase = t === opts.baseTarget;
|
||||
const name = isBase ? `${t.ref} (база)` : t.ref;
|
||||
if (res.fatal) {
|
||||
md.push(`| ${name} | ${short(t.sha)} | ОШИБКА: ${res.fatal} | | | | |`);
|
||||
continue;
|
||||
}
|
||||
const check = res.check
|
||||
? res.check.ok
|
||||
? `OK (${res.check.tests})`
|
||||
: `FAIL exit=${res.check.code}: ${res.check.error}`
|
||||
: 'не запускался';
|
||||
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
|
||||
? `${greens}/${res.integration.length} зелёных${reds.length ? `; FAIL: ${reds.join(', ')}` : ''}${skips.length ? `; SKIP (файл пропущен целиком): ${skips.join(', ')}` : ''}`
|
||||
: 'не запускались';
|
||||
let e2e = 'не запускался';
|
||||
let green = '';
|
||||
let red = '';
|
||||
if (res.e2e?.map) {
|
||||
e2e = `${res.e2e.red}/${res.e2e.green}`;
|
||||
if (!isBase && baseRes.e2e?.map) {
|
||||
const d = diffE2e(baseRes.e2e.map, res.e2e.map);
|
||||
green = d.turnedGreen.join(', ') || '—';
|
||||
red = [...d.turnedRed, ...d.other].join('; ') || '—';
|
||||
}
|
||||
} else if (res.e2e) e2e = `стенд не поднялся: ${res.e2e.error}`;
|
||||
if (isBase) {
|
||||
green = 'n/a';
|
||||
red = 'n/a';
|
||||
}
|
||||
md.push(
|
||||
`| ${name} | ${short(t.sha)} | ${check} | ${integration} | ${e2e} | ${green} | ${red} |`,
|
||||
);
|
||||
}
|
||||
md.push('');
|
||||
if (
|
||||
results.values().next().value.integration ||
|
||||
targets.some((t) => results.get(t).integration)
|
||||
) {
|
||||
md.push('### Интеграционные тесты (каждый файл отдельно, свежая БД)');
|
||||
md.push('');
|
||||
md.push('| Ветка | Файл | Итог | Test Files | Tests | Первая ошибка |');
|
||||
md.push('|---|---|---|---|---|---|');
|
||||
for (const res of results.values()) {
|
||||
for (const r of res.integration ?? []) {
|
||||
md.push(
|
||||
`| ${res.target.ref} | ${r.file} | ${r.ok ? 'OK' : r.skipped ? 'SKIP' : 'FAIL'} | ${r.files} | ${r.tests} | ${r.error} |`,
|
||||
);
|
||||
}
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
const overlayNotes = [...results.values()].filter((r) => r.wt?.overlayTouched.length);
|
||||
if (overlayNotes.length) {
|
||||
md.push('Внимание: ветка меняла файлы стенда, при приёмке они заменены оверлеем:');
|
||||
for (const r of overlayNotes) md.push(`- ${r.target.ref}: ${r.wt.overlayTouched.join(', ')}`);
|
||||
md.push('');
|
||||
}
|
||||
const npmCi = [...results.values()].filter((r) => r.wt?.deps === 'npm ci');
|
||||
if (npmCi.length)
|
||||
md.push(`npm ci (lock отличается): ${npmCi.map((r) => r.target.ref).join(', ')}\n`);
|
||||
}
|
||||
|
||||
if (targets.length > 0) {
|
||||
md.push('### Попарные слияния (`git merge-tree --write-tree`)');
|
||||
md.push('');
|
||||
md.push('| Ветка A | Ветка B | Результат | Конфликтные файлы |');
|
||||
md.push('|---|---|---|---|');
|
||||
const pairs = [];
|
||||
for (const t of targets) pairs.push([opts.baseTarget, t]);
|
||||
for (let i = 0; i < targets.length; i += 1) {
|
||||
for (let j = i + 1; j < targets.length; j += 1) pairs.push([targets[i], targets[j]]);
|
||||
}
|
||||
for (const [a, b] of pairs) {
|
||||
const r = mergeTree(a, b);
|
||||
md.push(`| ${a.ref} | ${b.ref} | ${r.status} | ${r.files.join(', ')} |`);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
if (formatResults.length) {
|
||||
md.push('### Форматирование без поведения');
|
||||
md.push('');
|
||||
for (const f of formatResults) {
|
||||
const verdict = f.rows.length === 0 ? 'ТОЛЬКО ФОРМАТИРОВАНИЕ' : 'ЕСТЬ ИЗМЕНЕНИЯ СВЕРХ PRETTIER';
|
||||
md.push(
|
||||
`**${f.target.ref}** (${short(f.target.sha)}), merge-base ${short(f.mergeBase)}, изменено файлов: ${f.files} — ${verdict}`,
|
||||
);
|
||||
md.push('');
|
||||
if (f.rows.length) {
|
||||
md.push('| Файл | Почему | Строк сверх prettier | Эти же строки в ветке |');
|
||||
md.push('|---|---|---|---|');
|
||||
for (const r of f.rows) {
|
||||
md.push(`| ${r.file} | ${r.verdict} | ${r.lines} | ${r.same.join('; ') || '—'} |`);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = redact(md.join('\n'));
|
||||
writeFileSync(path.join(logDir, 'summary.md'), text);
|
||||
if (opts.out) writeFileSync(opts.out, text);
|
||||
console.log(text);
|
||||
|
|
@ -14,13 +14,9 @@ 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';
|
||||
// --roles keep: роли уже созданы (например, предыдущим вызовом accept-branch) — пароли НЕ меняются,
|
||||
// только пересоздаётся БД. Нужно, чтобы параллельные/последовательные стенды не ломали друг другу пароли.
|
||||
const keepRoles = args.get('--roles') === 'keep';
|
||||
if (!adminUrl || (!envFile && !keepRoles)) {
|
||||
if (!adminUrl || !envFile) {
|
||||
console.error(
|
||||
'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',
|
||||
'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out <env-path> [--db name]',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
|
@ -35,14 +31,7 @@ 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(`
|
||||
await admin.query(`
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN
|
||||
CREATE ROLE krona_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
|
||||
|
|
@ -51,17 +40,16 @@ export async function provision() {
|
|||
CREATE ROLE krona_staff_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
||||
END IF;
|
||||
END $$`);
|
||||
const alterMigrator = await admin.query(
|
||||
"SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql",
|
||||
[migratorPassword],
|
||||
);
|
||||
await admin.query(alterMigrator.rows[0].sql);
|
||||
const alterApp = await admin.query(
|
||||
"SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql",
|
||||
[appPassword],
|
||||
);
|
||||
await admin.query(alterApp.rows[0].sql);
|
||||
}
|
||||
const alterMigrator = await admin.query(
|
||||
"SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql",
|
||||
[migratorPassword],
|
||||
);
|
||||
await admin.query(alterMigrator.rows[0].sql);
|
||||
const alterApp = await admin.query(
|
||||
"SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql",
|
||||
[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],
|
||||
|
|
@ -85,10 +73,6 @@ 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());
|
||||
|
|
|
|||
|
|
@ -77,10 +77,7 @@ const secretPatterns = [
|
|||
/kstaff_dev_[0-9A-Z]{26}_[0-9a-f]{16,}/,
|
||||
/postgres:\/\/[^\s:@]+:[^\s@]+@/,
|
||||
];
|
||||
// Только файлы верхнего уровня: в подкаталогах (accept/ от accept-branch.mjs) свои логи с редакцией.
|
||||
for (const entry of readdirSync(evidence, { withFileTypes: true })) {
|
||||
if (!entry.isFile()) continue;
|
||||
const name = entry.name;
|
||||
for (const name of readdirSync(evidence)) {
|
||||
const content = readFileSync(path.join(evidence, name), 'utf8');
|
||||
for (const pattern of secretPatterns) {
|
||||
if (pattern.test(content))
|
||||
|
|
|
|||
|
|
@ -1,543 +0,0 @@
|
|||
// PILOT-REHOME (TASK-GROK-3 п.5): приёмочный сценарий для будущего scripts/rehome-tenant.mjs
|
||||
// (TASK-CODEX-2 п.2.2) — перенос данных пилота с заглушки арендатора на настоящий tenant.
|
||||
// Предполагаемый контракт (NOTES-FOR-EXECUTORS.md, C6):
|
||||
// DATABASE_ADMIN_URL=<админ БД> node scripts/rehome-tenant.mjs --from <ulid> --to <ulid> [--dry-run] [--json]
|
||||
// --json печатает (последней строкой) {"tables":{"<таблица>":<строк>},"ledger_amount_minor_sum":<число>}.
|
||||
// Таблицы берутся из information_schema.columns (column_name = 'tenant_id'), а не списком вручную:
|
||||
// новая таблица без сида делает красным [PILOT-REHOME-SEED], чтобы перенос не забыл её.
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import pg from 'pg';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ctx, dbQueryAs } from './harness/client.js';
|
||||
import { randomUlid } from './harness/context.js';
|
||||
import { baseEnv } from '../../scripts/test/node-commands.mjs';
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, '../..');
|
||||
const scriptPath = path.join(repoRoot, 'scripts/rehome-tenant.mjs');
|
||||
const STUB = '01J00000000000000000000000';
|
||||
const TARGET = '01M2R6AC5652NPT3MG34DKN4ZP';
|
||||
const FOREIGN = randomUlid();
|
||||
|
||||
type RehomeReport = { tables?: Record<string, number>; ledger_amount_minor_sum?: number | string };
|
||||
type Snapshot = { counts: Record<string, number>; ledgerSum: string };
|
||||
|
||||
async function withMigrator<T>(
|
||||
tenant: string,
|
||||
fn: (client: pg.Client) => Promise<T>,
|
||||
userId?: string,
|
||||
connectionString = ctx().migrationDatabaseUrl,
|
||||
): Promise<T> {
|
||||
const client = new pg.Client({ connectionString });
|
||||
await client.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenant]);
|
||||
// Политики INSERT журнала аудита, начислений, отметок и табеля требуют app.user_id = автор.
|
||||
if (userId) await client.query("SELECT set_config('app.user_id', $1, true)", [userId]);
|
||||
const result = await fn(client);
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => undefined);
|
||||
throw error;
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function tenantTables(): Promise<string[]> {
|
||||
return withMigrator(STUB, async (client) => {
|
||||
const result = await client.query<{ table_name: string }>(
|
||||
`SELECT DISTINCT c.table_name FROM information_schema.columns c
|
||||
JOIN information_schema.tables t USING (table_schema, table_name)
|
||||
WHERE c.table_schema = 'public' AND c.column_name = 'tenant_id' AND t.table_type = 'BASE TABLE'
|
||||
ORDER BY 1`,
|
||||
);
|
||||
return result.rows.map((row) => row.table_name);
|
||||
});
|
||||
}
|
||||
|
||||
async function snapshot(tenant: string, tables: string[]): Promise<Snapshot> {
|
||||
return withMigrator(tenant, async (client) => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const table of tables) {
|
||||
const result = await client.query<{ n: number }>(
|
||||
`SELECT count(*)::int AS n FROM ${table} WHERE tenant_id = $1`,
|
||||
[tenant],
|
||||
);
|
||||
counts[table] = result.rows[0]!.n;
|
||||
}
|
||||
const sum = await client.query<{ s: string }>(
|
||||
'SELECT coalesce(sum(amount_minor), 0)::text AS s FROM staff_payroll_ledger WHERE tenant_id = $1',
|
||||
[tenant],
|
||||
);
|
||||
return { counts, ledgerSum: sum.rows[0]!.s };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет строки арендатора из всех таблиц (дочерние раньше родительских — повторными проходами).
|
||||
* У staff_payroll_ledger нет политики DELETE, поэтому под FORCE RLS мигратор его не очистит:
|
||||
* при наличии админа БД чистит админ, иначе остаток ловит проверка ниже.
|
||||
*/
|
||||
async function purge(tenant: string, tables: string[]) {
|
||||
const connection = ctx().adminDatabaseUrl ?? ctx().migrationDatabaseUrl;
|
||||
await withMigrator(
|
||||
tenant,
|
||||
async (client) => {
|
||||
let pending = [...tables];
|
||||
for (let pass = 0; pass < tables.length && pending.length > 0; pass += 1) {
|
||||
const failed: string[] = [];
|
||||
for (const table of pending) {
|
||||
await client.query('SAVEPOINT purge');
|
||||
try {
|
||||
await client.query(`DELETE FROM ${table} WHERE tenant_id = $1`, [tenant]);
|
||||
await client.query('RELEASE SAVEPOINT purge');
|
||||
} catch {
|
||||
await client.query('ROLLBACK TO SAVEPOINT purge');
|
||||
failed.push(table);
|
||||
}
|
||||
}
|
||||
pending = failed;
|
||||
}
|
||||
if (pending.length) throw new Error(`не удалось очистить ${tenant}: ${pending.join(', ')}`);
|
||||
},
|
||||
undefined,
|
||||
connection,
|
||||
);
|
||||
const left = Object.entries((await snapshot(tenant, tables)).counts).filter(([, n]) => n > 0);
|
||||
if (left.length) {
|
||||
throw new Error(
|
||||
`после очистки у ${tenant} остались строки (нужен админ БД: E2E_DB_ADMIN_URL): ${left.map(([t, n]) => `${t}=${n}`).join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hex64 = () => randomBytes(32).toString('hex');
|
||||
const code = (prefix: string) => `${prefix}_${randomBytes(4).toString('hex')}`;
|
||||
|
||||
/**
|
||||
* Синтетические данные арендатора: по 2+ строки в каждой известной таблице с tenant_id.
|
||||
* Возвращает id сотрудников и список засеянных таблиц. Пишет мигратор (владелец таблиц),
|
||||
* FORCE RLS действует и на него, поэтому app.tenant_id выставлен в транзакции.
|
||||
*/
|
||||
async function seed(tenant: string) {
|
||||
const ids = (n: number) => Array.from({ length: n }, () => randomUlid());
|
||||
const actor = randomUlid();
|
||||
const corr = () => randomUlid();
|
||||
const seeded = new Set<string>();
|
||||
const employees = ids(3);
|
||||
const amounts = [150_000n, 72_300n, -12_300n].map(
|
||||
(base) => base + BigInt(randomBytes(2).readUInt16BE(0)),
|
||||
);
|
||||
await withMigrator(
|
||||
tenant,
|
||||
async (client) => {
|
||||
const q = async (table: string, sql: string, params: unknown[]) => {
|
||||
await client.query(sql, params);
|
||||
seeded.add(table);
|
||||
};
|
||||
const [ou1, ou2] = ids(2);
|
||||
for (const [i, ou] of [ou1, ou2].entries()) {
|
||||
await q(
|
||||
'staff_org_units',
|
||||
'INSERT INTO staff_org_units (id, tenant_id, code, name) VALUES ($1,$2,$3,$4)',
|
||||
[ou, tenant, `rehome-${i}-${randomBytes(3).toString('hex')}`, `Точка ${i + 1}`],
|
||||
);
|
||||
}
|
||||
const professions = ids(2);
|
||||
for (const [i, id] of professions.entries()) {
|
||||
await q(
|
||||
'staff_professions',
|
||||
'INSERT INTO staff_professions (id, tenant_id, code, name) VALUES ($1,$2,$3,$4)',
|
||||
[id, tenant, code('prof'), `Профессия ${i}-${randomBytes(3).toString('hex')}`],
|
||||
);
|
||||
}
|
||||
for (const [i, id] of employees.entries()) {
|
||||
await q(
|
||||
'staff_employees',
|
||||
'INSERT INTO staff_employees (id, tenant_id, personnel_number, display_name, primary_org_unit_id, profession_id) VALUES ($1,$2,$3,$4,$5,$6)',
|
||||
[
|
||||
id,
|
||||
tenant,
|
||||
`R-${randomBytes(4).toString('hex')}`,
|
||||
`Сотрудник ${i + 1}`,
|
||||
i % 2 ? ou2 : ou1,
|
||||
professions[i % 2],
|
||||
],
|
||||
);
|
||||
}
|
||||
const metricKeys = [code('metric'), code('metric')];
|
||||
for (const key of metricKeys) {
|
||||
await q(
|
||||
'staff_metric_definitions',
|
||||
'INSERT INTO staff_metric_definitions (id, tenant_id, key, label) VALUES ($1,$2,$3,$4)',
|
||||
[randomUlid(), tenant, key, `Метрика ${key}`],
|
||||
);
|
||||
}
|
||||
for (const [i, key] of metricKeys.entries()) {
|
||||
await q(
|
||||
'staff_daily_metrics',
|
||||
'INSERT INTO staff_daily_metrics (id, tenant_id, org_unit_id, business_date, metric_key, value, recorded_by) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[randomUlid(), tenant, ou1, `2026-09-0${i + 1}`, key, 1000 + i, actor],
|
||||
);
|
||||
}
|
||||
for (const [i, ou] of [ou1, ou2].entries()) {
|
||||
await q(
|
||||
'staff_org_unit_terms',
|
||||
'INSERT INTO staff_org_unit_terms (id, tenant_id, org_unit_id, effective_from, created_by) VALUES ($1,$2,$3,$4,$5)',
|
||||
[randomUlid(), tenant, ou, `2026-0${i + 1}-01`, actor],
|
||||
);
|
||||
}
|
||||
const rules = ids(2);
|
||||
for (const [i, id] of rules.entries()) {
|
||||
await q(
|
||||
'staff_pay_rules',
|
||||
'INSERT INTO staff_pay_rules (id, tenant_id, code, version, name, rule, effective_from, created_by) VALUES ($1,$2,$3,1,$4,$5,$6,$7)',
|
||||
[
|
||||
id,
|
||||
tenant,
|
||||
code('rule'),
|
||||
`Правило ${i}`,
|
||||
JSON.stringify({ kind: 'hourly', rate_minor: 12_300 }),
|
||||
'2026-01-01',
|
||||
actor,
|
||||
],
|
||||
);
|
||||
}
|
||||
for (const [i, emp] of employees.slice(0, 2).entries()) {
|
||||
await q(
|
||||
'staff_pay_assignments',
|
||||
'INSERT INTO staff_pay_assignments (id, tenant_id, employee_id, pay_rule_id, effective_from, created_by) VALUES ($1,$2,$3,$4,$5,$6)',
|
||||
[randomUlid(), tenant, emp, rules[i], '2026-01-01', actor],
|
||||
);
|
||||
}
|
||||
for (const [i, emp] of employees.slice(0, 2).entries()) {
|
||||
await q(
|
||||
'staff_attendance_events',
|
||||
'INSERT INTO staff_attendance_events (id, tenant_id, employee_id, event_type, source, source_event_id, occurred_at, actor_id, correlation_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)',
|
||||
[
|
||||
randomUlid(),
|
||||
tenant,
|
||||
emp,
|
||||
i ? 'check_out' : 'check_in',
|
||||
'terminal',
|
||||
`src-${randomBytes(6).toString('hex')}`,
|
||||
'2026-09-01T09:00:00Z',
|
||||
actor,
|
||||
corr(),
|
||||
],
|
||||
);
|
||||
await q(
|
||||
'staff_work_sessions',
|
||||
'INSERT INTO staff_work_sessions (id, tenant_id, employee_id, org_unit_id, business_date) VALUES ($1,$2,$3,$4,$5)',
|
||||
[randomUlid(), tenant, emp, ou1, '2026-09-01'],
|
||||
);
|
||||
await q(
|
||||
'staff_timesheet_entries',
|
||||
'INSERT INTO staff_timesheet_entries (id, tenant_id, employee_id, work_date, updated_by) VALUES ($1,$2,$3,$4,$5)',
|
||||
[randomUlid(), tenant, emp, '2026-09-01', actor],
|
||||
);
|
||||
await q(
|
||||
'staff_work_items',
|
||||
'INSERT INTO staff_work_items (id, tenant_id, employee_id, org_unit_id, business_date, item_kind, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[randomUlid(), tenant, emp, ou1, '2026-09-01', 'rehome_item', actor],
|
||||
);
|
||||
}
|
||||
const runs = ids(2);
|
||||
for (const [i, run] of runs.entries()) {
|
||||
await q(
|
||||
'staff_payroll_runs',
|
||||
'INSERT INTO staff_payroll_runs (id, tenant_id, period_start, period_end, created_by) VALUES ($1,$2,$3,$4,$5)',
|
||||
[run, tenant, `2026-0${i + 7}-01`, `2026-0${i + 7}-28`, actor],
|
||||
);
|
||||
await q(
|
||||
'staff_payroll_lines',
|
||||
'INSERT INTO staff_payroll_lines (id, tenant_id, run_id, employee_id, org_unit_id, business_date, pay_rule_id, rule_code) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)',
|
||||
[randomUlid(), tenant, run, employees[i], ou1, `2026-0${i + 7}-02`, rules[i], 'hourly'],
|
||||
);
|
||||
await q(
|
||||
'staff_payouts',
|
||||
'INSERT INTO staff_payouts (id, tenant_id, employee_id, paid_on, amount_minor, created_by) VALUES ($1,$2,$3,$4,$5,$6)',
|
||||
[randomUlid(), tenant, employees[i], `2026-0${i + 8}-05`, 50_000 + i, actor],
|
||||
);
|
||||
}
|
||||
for (const [i, amount] of amounts.entries()) {
|
||||
await q(
|
||||
'staff_payroll_ledger',
|
||||
'INSERT INTO staff_payroll_ledger (id, tenant_id, employee_id, business_date, kind, amount_minor, ref_type, correlation_id, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)',
|
||||
[
|
||||
randomUlid(),
|
||||
tenant,
|
||||
employees[i % 2],
|
||||
'2026-09-01',
|
||||
amount < 0n ? 'adjustment' : 'accrual',
|
||||
amount.toString(),
|
||||
'manual',
|
||||
corr(),
|
||||
actor,
|
||||
],
|
||||
);
|
||||
}
|
||||
const outbox = ids(2);
|
||||
for (const id of outbox) {
|
||||
await q(
|
||||
'staff_outbox',
|
||||
'INSERT INTO staff_outbox (id, tenant_id, event_type, event_version, subject_type, subject_id, actor_type, actor_id, payload, correlation_id, occurred_at) VALUES ($1,$2,$3,1,$4,$5,$6,$7,$8,$9,now())',
|
||||
[id, tenant, 'staff.rehome_seed', 'employee', employees[0], 'user', actor, '{}', corr()],
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
await q(
|
||||
'staff_inbox',
|
||||
'INSERT INTO staff_inbox (event_id, tenant_id, event_type, event_version, correlation_id) VALUES ($1,$2,$3,1,$4)',
|
||||
[randomUlid(), tenant, 'core.rehome_seed', corr()],
|
||||
);
|
||||
await q(
|
||||
'staff_idempotency_keys',
|
||||
"INSERT INTO staff_idempotency_keys (tenant_id, key, request_hash, expires_at) VALUES ($1,$2,$3, now() + interval '1 day')",
|
||||
[tenant, `idem-${randomBytes(8).toString('hex')}`, hex64()],
|
||||
);
|
||||
const endpoint = `https://push.example.test/${randomBytes(8).toString('hex')}`;
|
||||
await q(
|
||||
'staff_push_subscriptions',
|
||||
'INSERT INTO staff_push_subscriptions (id, tenant_id, employee_id, endpoint, p256dh, auth) VALUES ($1,$2,$3,$4,$5,$6)',
|
||||
[
|
||||
randomUlid(),
|
||||
tenant,
|
||||
employees[i],
|
||||
endpoint,
|
||||
randomBytes(16).toString('base64url'),
|
||||
randomBytes(8).toString('base64url'),
|
||||
],
|
||||
);
|
||||
await q(
|
||||
'staff_notification_deliveries',
|
||||
'INSERT INTO staff_notification_deliveries (id, tenant_id, employee_id, outbox_id, endpoint) VALUES ($1,$2,$3,$4,$5)',
|
||||
[randomUlid(), tenant, employees[i], outbox[i], endpoint],
|
||||
);
|
||||
await q(
|
||||
'staff_standalone_employee_access',
|
||||
'INSERT INTO staff_standalone_employee_access (id, tenant_id, employee_id, subject_id, token_hash, token_hint, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[
|
||||
randomUlid(),
|
||||
tenant,
|
||||
employees[i],
|
||||
randomUlid(),
|
||||
createHash('sha256').update(randomBytes(32)).digest('hex'),
|
||||
'e2e…',
|
||||
actor,
|
||||
],
|
||||
);
|
||||
await q(
|
||||
'staff_sync_change_log',
|
||||
'INSERT INTO staff_sync_change_log (id, tenant_id, entity_type, entity_id, operation) VALUES ($1,$2,$3,$4,$5)',
|
||||
[randomUlid(), tenant, 'employee', employees[i], 'upsert'],
|
||||
);
|
||||
await q(
|
||||
'staff_audit_log',
|
||||
'INSERT INTO staff_audit_log (id, tenant_id, actor_id, action, entity_type, entity_id, correlation_id) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[randomUlid(), tenant, actor, 'employee.seeded', 'employee', employees[i], corr()],
|
||||
);
|
||||
}
|
||||
const devices = ids(2);
|
||||
for (const device of devices) {
|
||||
await q(
|
||||
'staff_sync_devices',
|
||||
'INSERT INTO staff_sync_devices (id, tenant_id, device_code, name, token_hash, token_hint, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[
|
||||
device,
|
||||
tenant,
|
||||
`dev-${randomBytes(4).toString('hex')}`,
|
||||
'Терминал',
|
||||
hex64(),
|
||||
'kstaff…',
|
||||
actor,
|
||||
],
|
||||
);
|
||||
await q(
|
||||
'staff_sync_command_inbox',
|
||||
'INSERT INTO staff_sync_command_inbox (id, tenant_id, device_id, command_type, idempotency_key, correlation_id, status, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7,now())',
|
||||
[
|
||||
randomUlid(),
|
||||
tenant,
|
||||
device,
|
||||
'record_attendance',
|
||||
`idem-${randomBytes(6).toString('hex')}`,
|
||||
corr(),
|
||||
'applied',
|
||||
],
|
||||
);
|
||||
await q(
|
||||
'staff_sync_device_checkpoints',
|
||||
'INSERT INTO staff_sync_device_checkpoints (device_id, tenant_id) VALUES ($1,$2)',
|
||||
[device, tenant],
|
||||
);
|
||||
}
|
||||
},
|
||||
actor,
|
||||
);
|
||||
return { employees, seeded };
|
||||
}
|
||||
|
||||
function runRehome(args: string[], adminUrl: string) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[scriptPath, '--from', STUB, '--to', TARGET, ...args],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: { ...baseEnv(), DATABASE_ADMIN_URL: adminUrl },
|
||||
encoding: 'utf8',
|
||||
timeout: 120_000,
|
||||
},
|
||||
);
|
||||
const lines = (result.stdout ?? '').trim().split('\n');
|
||||
let report: RehomeReport | null = null;
|
||||
for (const line of [...lines].reverse()) {
|
||||
if (!line.trim().startsWith('{')) continue;
|
||||
try {
|
||||
report = JSON.parse(line) as RehomeReport;
|
||||
break;
|
||||
} catch {
|
||||
// не JSON — ищем дальше
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: result.status,
|
||||
stdout: result.stdout ?? '',
|
||||
stderr: result.stderr ?? '',
|
||||
report,
|
||||
};
|
||||
}
|
||||
|
||||
const state: {
|
||||
tables: string[];
|
||||
employees: string[];
|
||||
before?: { stub: Snapshot; foreign: Snapshot };
|
||||
} = { tables: [], employees: [] };
|
||||
|
||||
describe('PILOT-REHOME: перенос заглушки арендатора на настоящий tenant', () => {
|
||||
it('[PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema)', async () => {
|
||||
state.tables = await tenantTables();
|
||||
expect(state.tables.length).toBeGreaterThan(0);
|
||||
await purge(STUB, state.tables);
|
||||
await purge(TARGET, state.tables);
|
||||
const stub = await seed(STUB);
|
||||
const foreign = await seed(FOREIGN);
|
||||
state.employees = stub.employees;
|
||||
const unseeded = state.tables.filter((table) => !stub.seeded.has(table));
|
||||
expect(
|
||||
unseeded,
|
||||
`таблицы с tenant_id без сида в tests/e2e/60-pilot-rehome.e2e.ts — перенос арендатора их не проверит: ${unseeded.join(', ')}`,
|
||||
).toEqual([]);
|
||||
const stubSnap = await snapshot(STUB, state.tables);
|
||||
const foreignSnap = await snapshot(FOREIGN, state.tables);
|
||||
const empty = state.tables.filter((t) => stubSnap.counts[t]! < 1 || foreignSnap.counts[t]! < 1);
|
||||
expect(empty, `сид не виден через RLS: ${empty.join(', ')}`).toEqual([]);
|
||||
expect(Object.values((await snapshot(TARGET, state.tables)).counts).every((n) => n === 0)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(foreign.seeded.size).toBe(stub.seeded.size);
|
||||
state.before = { stub: stubSnap, foreign: foreignSnap };
|
||||
});
|
||||
|
||||
it('[PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает', async () => {
|
||||
const before = state.before;
|
||||
expect(before, 'сид не выполнен (см. [PILOT-REHOME-SEED])').toBeDefined();
|
||||
if (!existsSync(scriptPath)) {
|
||||
throw new Error(
|
||||
'scripts/rehome-tenant.mjs отсутствует (TASK-CODEX-2 п.2.2): перенос заглушки ' +
|
||||
`${STUB} → ${TARGET} не реализован; контракт сценария — NOTES-FOR-EXECUTORS.md C6. ` +
|
||||
`Засеяно таблиц: ${state.tables.length}, ledger SUM(amount_minor)=${before!.stub.ledgerSum}`,
|
||||
);
|
||||
}
|
||||
const adminUrl = ctx().adminDatabaseUrl;
|
||||
if (!adminUrl) {
|
||||
throw new Error('нет админа БД: задайте E2E_DB_ADMIN_URL или E2E_PG_ADMIN_URL');
|
||||
}
|
||||
const expectedTables = Object.fromEntries(
|
||||
Object.entries(before!.stub.counts).filter(([, n]) => n > 0),
|
||||
);
|
||||
|
||||
// 1. --dry-run: печатает те же числа и ничего не меняет.
|
||||
const dry = runRehome(['--dry-run', '--json'], adminUrl);
|
||||
expect(dry.status, `dry-run exit: ${dry.stderr}`).toBe(0);
|
||||
expect(dry.report, `dry-run без JSON-отчёта: ${dry.stdout}`).not.toBeNull();
|
||||
const dryTables = Object.fromEntries(
|
||||
Object.entries(dry.report!.tables ?? {}).filter(([, n]) => Number(n) > 0),
|
||||
);
|
||||
expect(dryTables).toEqual(expectedTables);
|
||||
expect(String(dry.report!.ledger_amount_minor_sum)).toBe(before!.stub.ledgerSum);
|
||||
expect(await snapshot(STUB, state.tables), '--dry-run изменил данные заглушки').toEqual(
|
||||
before!.stub,
|
||||
);
|
||||
expect(Object.values((await snapshot(TARGET, state.tables)).counts).every((n) => n === 0)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
// 2. Перенос ролью администратора БД.
|
||||
const real = runRehome(['--json'], adminUrl);
|
||||
expect(real.status, `перенос exit: ${real.stderr}`).toBe(0);
|
||||
const afterStub = await snapshot(STUB, state.tables);
|
||||
const afterTarget = await snapshot(TARGET, state.tables);
|
||||
const leftovers = state.tables.filter((t) => afterStub.counts[t] !== 0);
|
||||
expect(leftovers, 'под заглушкой остались строки').toEqual([]);
|
||||
for (const table of state.tables) {
|
||||
const expected = before!.stub.counts[table]!;
|
||||
if (table === 'staff_audit_log')
|
||||
expect(
|
||||
afterTarget.counts[table]!,
|
||||
'staff_audit_log: у нового арендатора нет новой записи о переносе',
|
||||
).toBeGreaterThan(expected);
|
||||
else expect(afterTarget.counts[table], table).toBe(expected);
|
||||
}
|
||||
expect(afterTarget.ledgerSum).toBe(before!.stub.ledgerSum);
|
||||
expect(await snapshot(FOREIGN, state.tables), 'посторонний арендатор изменён').toEqual(
|
||||
before!.foreign,
|
||||
);
|
||||
const distinct = await withMigrator(TARGET, async (client) => {
|
||||
const found: string[] = [];
|
||||
for (const table of state.tables) {
|
||||
const r = await client.query<{ tenant_id: string }>(
|
||||
`SELECT DISTINCT tenant_id FROM ${table} WHERE tenant_id IN ($1, $2)`,
|
||||
[STUB, TARGET],
|
||||
);
|
||||
found.push(...r.rows.map((row) => `${table}:${row.tenant_id}`));
|
||||
}
|
||||
return found;
|
||||
});
|
||||
expect(distinct.filter((d) => d.endsWith(STUB))).toEqual([]);
|
||||
|
||||
// 3. Запись аудита у нового арендатора.
|
||||
const audit = await dbQueryAs<{ action: string }>(
|
||||
TARGET,
|
||||
"SELECT action FROM staff_audit_log WHERE tenant_id = $1 AND action ILIKE '%rehome%'",
|
||||
[TARGET],
|
||||
);
|
||||
expect(
|
||||
audit.length,
|
||||
'нет записи staff_audit_log о переносе у нового арендатора',
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// 4. Роль приложения видит тех же сотрудников под новым tenant и ничего под старым.
|
||||
const underTarget = await dbQueryAs<{ id: string }>(
|
||||
TARGET,
|
||||
'SELECT id FROM staff_employees ORDER BY id',
|
||||
);
|
||||
expect(underTarget.map((row) => row.id)).toEqual([...state.employees].sort());
|
||||
expect(await dbQueryAs(STUB, 'SELECT id FROM staff_employees')).toEqual([]);
|
||||
|
||||
// 5. Журнал начислений по-прежнему append-only для приложения.
|
||||
await expect(
|
||||
dbQueryAs(TARGET, 'UPDATE staff_payroll_ledger SET amount_minor = amount_minor + 1'),
|
||||
).rejects.toThrow(/permission denied|append-only|read-only/i);
|
||||
|
||||
// 6. Повторный запуск отказывает и ничего не меняет.
|
||||
const again = runRehome(['--json'], adminUrl);
|
||||
expect(again.status, 'повторный перенос должен отказать').not.toBe(0);
|
||||
expect(await snapshot(TARGET, state.tables)).toEqual(afterTarget);
|
||||
});
|
||||
});
|
||||
|
|
@ -86,36 +86,6 @@ ID в названиях тестов — двойные «владелец/мо
|
|||
неизвестный ключ → 400. `GET .../pay-assignment` возвращает их в `params`.
|
||||
- Старые поля `rate_minor`/`percent_bp`/`guarantee_minor` можно оставить как сокращения.
|
||||
|
||||
### C6. Перенос арендатора пилота (PILOT-REHOME, TASK-CODEX-2 п.2.2)
|
||||
|
||||
Что проверяет `tests/e2e/60-pilot-rehome.e2e.ts` (`[PILOT-REHOME]`):
|
||||
|
||||
- Запуск: `DATABASE_ADMIN_URL=<админ БД> node scripts/rehome-tenant.mjs --from 01J00000000000000000000000 --to 01M2R6AC5652NPT3MG34DKN4ZP [--dry-run] [--json]`.
|
||||
Стенд передаёт `DATABASE_ADMIN_URL` суперпользователя в БД стенда (`E2E_DB_ADMIN_URL` или
|
||||
`E2E_PG_ADMIN_URL` с именем БД).
|
||||
- `--json`: последней строкой stdout — `{"tables":{"<таблица>":<строк у --from>},"ledger_amount_minor_sum":<число или строка>}`.
|
||||
Таблицы с нулём можно не печатать.
|
||||
- `--dry-run`: exit 0, те же числа, в БД ничего не меняется.
|
||||
- Перенос: exit 0; под `--from` 0 строк во **всех** таблицах с `tenant_id` (список из
|
||||
`information_schema.columns`, не вручную); у `--to` те же счётчики и та же
|
||||
`SUM(amount_minor)` в `staff_payroll_ledger`; другие арендаторы не тронуты.
|
||||
- В `staff_audit_log` у нового арендатора появляется запись с `action`, содержащим `rehome`
|
||||
(например, `tenant.rehomed`, старый tenant — в `new_values`).
|
||||
- После переноса `krona_staff_app` видит тех же сотрудников (те же `id`) под новым tenant и никого
|
||||
под старым; `UPDATE staff_payroll_ledger` ролью приложения по-прежнему отклоняется.
|
||||
- Повторный запуск (под `--from` пусто / у `--to` уже есть данные) — exit ≠ 0, ничего не меняет.
|
||||
|
||||
Что нужно учесть (видно по схеме, `pg_policy`/`pg_constraint` на PG17):
|
||||
|
||||
- У `staff_payroll_ledger` нет политики UPDATE/DELETE, у приложения нет права UPDATE, FORCE RLS
|
||||
действует и на владельца: `krona_migrator` строки журнала не перенесёт. Нужна роль с обходом RLS
|
||||
(суперпользователь/BYPASSRLS) — поэтому `DATABASE_ADMIN_URL`.
|
||||
- Внешние ключи составные `(tenant_id, …)` и не DEFERRABLE: переносить родителя раньше детей
|
||||
нельзя — нужен порядок, отложенные проверки или `session_replication_role = replica` в одной транзакции.
|
||||
- Политики INSERT `staff_audit_log`, `staff_payroll_ledger`, `staff_attendance_events`,
|
||||
`staff_timesheet_entries` требуют `actor_id/created_by/updated_by = current_setting('app.user_id')` —
|
||||
для записи аудита без обхода RLS выставить `app.user_id`.
|
||||
|
||||
## 2. Дефекты продукта, найденные сценариями (все красные сейчас)
|
||||
|
||||
Команда и фактический вывод — `tests/e2e/evidence/e2e-run.txt`; таблица — `tests/e2e/REPORT.md`.
|
||||
|
|
@ -219,35 +189,3 @@ CONFLICT DO NOTHING` со своим `runId` → строки/UPDATE по нес
|
|||
- Ни одна ветка не влита в основную (f5ac179), поэтому ID из `known-defects.json` не удалены.
|
||||
- Суммы в тестах кратны рублю: расчёт округляет строку до `rounding_unit_minor`
|
||||
(по умолчанию 100 коп.). Если округление должно быть другим — это решение продукта, не теста.
|
||||
|
||||
## 7. Круг 3 (TASK-GROK-3, 2026-09-26)
|
||||
|
||||
Команды и полный вывод — `tests/e2e/REPORT.md` («Круг 3») и `tests/e2e/report-evidence/round3-*.md`.
|
||||
Приёмка одной командой: `node scripts/test/accept-branch.mjs <ветка>…` (нужен `E2E_PG_ADMIN_URL`
|
||||
или готовые роли в `E2E_MIGRATOR_URL` + `E2E_APP_URL`).
|
||||
|
||||
- **Antigravity, `sync-bootstrap.test.ts` (8baad55)**: `Error: Called end on pool more than once` при
|
||||
`Tests 2 passed`. Причина: `app.close()` в тесте вызывает хук `onClose` → `database.close()`
|
||||
(`src/app.ts:148`), затем `afterAll` закрывает тот же `Database` ещё раз. На текущей голове
|
||||
`24cc18d` файл зелёный (`1 passed (1) | 2 passed (2)`).
|
||||
- **Antigravity, `sync-rejection.test.ts` (1df2019)**: `column "created_by" of relation "staff_employees"
|
||||
does not exist` в `beforeAll` (`Tests 2 skipped`). На `6c5bf36`/`b283736` исправлено.
|
||||
- **Конфликты**: на прежних головах `1df2019 × 8baad55` и `8baad55 × 513cdff` конфликтовали в
|
||||
`src/domain/sync/service.ts`; на текущих головах (fetch 16:16 NOVT) конфликтов нет ни в одной из 55 пар.
|
||||
- **Codex, `codex/fix-format-baseline`**: на `513cdff` в `service.ts`/`sync.test.ts` было 232/130 строк сверх
|
||||
prettier, все есть в `antigravity/fix-reject-device-shifts`; на `a05777c` — только форматирование (19 файлов).
|
||||
- **Codex, `live-postgres-remediation.test.ts`**: выполняется только при `DATABASE_TEST_ISOLATED=true`,
|
||||
`DATABASE_TEST_GUARD_TOKEN`, таблице `krona_staff_test_guard` с этим маркером и имени БД
|
||||
`krona_staff_audit_*`. Соглашение нигде, кроме самого теста, не описано, и ни CI, ни
|
||||
`scripts/test/*` его не выполняют — в `npm run check` файл всегда `skipped`. `accept-branch` ставит
|
||||
предохранитель в одноразовой БД: `1 passed (1)`. Предложение: описать соглашение в README/AGENTS
|
||||
и после слияния включить его в CI (могу сделать в своей зоне, `.forgejo/workflows/ci.yml`).
|
||||
- **N5 до сих пор в основе**: `tenant-isolation.test.ts` красный на `b0243d5` и всех ветках от
|
||||
`f5ac179` (`permission denied for table staff_employees`); исправление — в `grok/e2e-harness`.
|
||||
Локальная сборка основа + fix-live-database + fix-sync-c0-bootstrap + grok/e2e-harness:
|
||||
интеграционные 7/7, храповик просит убрать ровно K3/N2, K4/N1, K13/SYNC-A, K13/SYNC-B, SYNC-C0.
|
||||
- **Схема (низкий приоритет)**: `staff_notification_deliveries.outbox_id → staff_outbox(id)` — единственный
|
||||
внешний ключ без `tenant_id`; все остальные составные `(tenant_id, …)`. База не мешает доставке
|
||||
ссылаться на событие outbox другого арендатора (RLS фильтрует чтение, но не ссылку). Предложение:
|
||||
`UNIQUE (tenant_id, id)` на `staff_outbox` и FK `(tenant_id, outbox_id)`.
|
||||
- **PILOT-REHOME**: скрипта нет → `[PILOT-REHOME]` красный; контракт — C6.
|
||||
|
|
|
|||
|
|
@ -31,107 +31,6 @@ SUPERUSER, все токены и пароли случайные (`crypto.rando
|
|||
| §4 | Приёмка `antigravity/fix-reject-device-shifts`, `codex/fix-format-baseline`, `codex/fix-live-database` — таблицы ниже |
|
||||
| — | Синтетическая ставка 123,45 → 123,00 ₽/ч: расчёт округляет строку до рубля (`rounding_unit_minor`), тест не должен зависеть от округления (выяснилось на fix-live-database) |
|
||||
|
||||
## Круг 3 (TASK-GROK-3)
|
||||
|
||||
Ветка продолжена поверх `ba59a48` без переписывания истории (без rebase на `b0243d5`, без force-push).
|
||||
|
||||
| Пункт | Что сделано |
|
||||
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| п.1 | `scripts/test/accept-branch.mjs <ветка>…`: worktree на ветку в `../krona-staff-acceptance/` + оверлей текущего стенда (`tests/e2e/**`, `scripts/test/**`), `npm run check` без переменных БД, **каждый** `tests/integration/*.test.ts` отдельно на свежей мигрированной БД PG17 (красный — по строке `Test Files`, ловит ошибки `beforeAll/afterAll`; файл, где всё пропущено, — отдельный вердикт `SKIP`), e2e и разница с основой по ID, попарный `git merge-tree --write-tree` (все ветки между собой и с основой), итог в markdown. Логи — `tests/e2e/evidence/accept/<время>/` (пароли в URL заменены на `***`) |
|
||||
| п.1 | Готовые роли: если заданы `E2E_MIGRATOR_URL` и `E2E_APP_URL`, `global-setup` и `accept-branch` роли не создают и пароли не меняют (БД — из URL, сброс откатом всех миграций). По умолчанию как раньше. `provision-pg.mjs --roles keep` пересоздаёт только БД: `accept-branch` создаёт роли один раз на прогон |
|
||||
| п.1 | Предохранитель изолированной БД для `live-postgres-remediation.test.ts` (Codex): в admin-режиме интеграционная БД одноразовая (`krona_staff_audit_acc_it`), `krona_staff_test_guard` со случайным маркером, `DATABASE_TEST_ISOLATED=true` — без этого файл пропускается целиком |
|
||||
| п.2 | «Форматирование без поведения» (для веток с `format` в имени и `--format-check <ветка>`): версия из merge-base → prettier (конфиг основы, API из `node_modules`) → сравнение с веткой; для лишних строк — сколько из них есть в других переданных ветках |
|
||||
| п.3 | Прогон по всем открытым `codex/*`, `antigravity/*` — ниже |
|
||||
| п.4 | Храповик **не тронут**: `origin/codex/bootstrap-platform-foundation` = `b0243d5` (только docs поверх `f5ac179`); `git merge-base --is-ancestor origin/<ветка> origin/codex/bootstrap-platform-foundation` — `NOT merged` для fix-live-database, fix-reject-device-shifts, fix-sync-c0-bootstrap, fix-format-baseline |
|
||||
| п.5 | `[PILOT-REHOME-SEED]` (зелёный) и `[PILOT-REHOME]` (красный, в `known-defects.json`) — `tests/e2e/60-pilot-rehome.e2e.ts` |
|
||||
| — | `run-e2e.mjs`: проверка секретов в `evidence/` больше не падает `EISDIR` на подкаталоге `accept/` |
|
||||
|
||||
### п.1: ожидания владельца на SHA из задания
|
||||
|
||||
Команда: `node scripts/test/accept-branch.mjs 1df2019 antigravity/fix-sync-c0-bootstrap codex/fix-format-baseline`
|
||||
(16:04 NOVT; `1df2019` передан SHA, потому что ветка Antigravity к этому времени была перезаписана
|
||||
на `6c5bf36`). Полный вывод — [`report-evidence/round3-accept-owner-shas.md`](report-evidence/round3-accept-owner-shas.md).
|
||||
|
||||
| Ожидание владельца | Фактический вывод |
|
||||
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| sync-rejection.test.ts на 1df2019 FAIL `column "created_by" … does not exist` | `FAIL \| 1 failed (1) \| 2 skipped (2) \| error: column "created_by" of relation "staff_employees" does not exist` |
|
||||
| sync-bootstrap.test.ts на 8baad55 FAIL `Called end on pool more than once` при `Tests 2 passed` | `FAIL \| 1 failed (1) \| 2 passed (2) \| Error: Called end on pool more than once` |
|
||||
| SYNC-C0 зелёный на 8baad55 | e2e «стало зелёным»: `SYNC-C0` |
|
||||
| AG reject + AG c0 — конфликт в src/domain/sync/service.ts | `1df2019 \| origin/antigravity/fix-sync-c0-bootstrap \| КОНФЛИКТ \| src/domain/sync/service.ts` |
|
||||
| Codex format + AG c0 — конфликт | `origin/antigravity/fix-sync-c0-bootstrap \| origin/codex/fix-format-baseline \| КОНФЛИКТ \| src/domain/sync/service.ts` |
|
||||
| Codex format + AG reject — чисто | `1df2019 \| origin/codex/fix-format-baseline \| чисто` |
|
||||
|
||||
Все шесть совпали. На `6c5bf36` (первый прогон по именам веток,
|
||||
[`round3-accept-owner-branches-first.md`](report-evidence/round3-accept-owner-branches-first.md))
|
||||
`sync-rejection.test.ts` уже зелёный (`1 passed (1) | 2 passed (2)`).
|
||||
|
||||
### п.2: форматирование без поведения
|
||||
|
||||
Команда: `node scripts/test/accept-branch.mjs --format-check tmp/prettier-only-f5ac179 --format-check codex/fix-format-baseline`
|
||||
([`round3-format-proof.md`](report-evidence/round3-format-proof.md)).
|
||||
`tmp/prettier-only-f5ac179` (`c552d38`) — локальная синтетическая ветка: `prettier --write .` поверх
|
||||
`f5ac179`, 24 файла; **не запушена**.
|
||||
|
||||
| Ветка | Результат |
|
||||
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `tmp/prettier-only-f5ac179` (c552d38) | изменено файлов: 24 — ТОЛЬКО ФОРМАТИРОВАНИЕ |
|
||||
| `codex/fix-format-baseline` (513cdff, прежняя голова) | ЕСТЬ ИЗМЕНЕНИЯ СВЕРХ PRETTIER: `src/domain/sync/service.ts` 232 строки, `tests/unit/sync.test.ts` 130 строк; в `antigravity/fix-reject-device-shifts` есть 232/232 и 130/130 этих строк |
|
||||
| `codex/fix-format-baseline` (a05777c, текущая, 16:09 NOVT) | изменено файлов: 19 — ТОЛЬКО ФОРМАТИРОВАНИЕ |
|
||||
|
||||
### п.3: все открытые ветки
|
||||
|
||||
Команда: `node scripts/test/accept-branch.mjs <все origin/codex/* и origin/antigravity/* кроме основы>`
|
||||
после `git fetch` в 16:16 NOVT, стенд `6f408bc`. Полный вывод (интеграционные тесты по файлам, 55 пар
|
||||
merge-tree) — [`report-evidence/round3-accept-all-open.md`](report-evidence/round3-accept-all-open.md).
|
||||
Основа `b0243d5`: check OK, integration 3/4 (FAIL tenant-isolation), e2e 46 red / 5 green.
|
||||
|
||||
| Ветка | SHA | Влита в основу | check | integration | e2e red/green | Стало зелёным | Стало красным |
|
||||
| ------------------------------------ | ------- | -------------------------- | ----- | --------------------------------------------------------- | ------------- | ------------------------------- | -------------------------------------------------------------------------- |
|
||||
| antigravity/fix-reject-device-shifts | b283736 | нет (+1) | OK | 4/5, FAIL tenant-isolation | 44/7 | K13/SYNC-A, K13/SYNC-B | — |
|
||||
| antigravity/fix-sync-c0-bootstrap | 24cc18d | нет (+3, включает b283736) | OK | 5/6, FAIL tenant-isolation | 43/8 | K13/SYNC-A, K13/SYNC-B, SYNC-C0 | — |
|
||||
| codex/fix-format-baseline | a05777c | нет (+1, от f5ac179) | OK | 3/4, FAIL tenant-isolation | 46/5 | — | — |
|
||||
| codex/fix-live-database | 1573860 | нет (+2, на a05777c) | OK | 4/5, FAIL tenant-isolation (live-postgres-remediation OK) | 44/7 | K3/N2, K4/N1 | — |
|
||||
| antigravity/employee-notifications | 3e20544 | да (предок) | OK | 3/4 | 49/2 | — | HARNESS, PILOT-REHOME-SEED, SYNC-L (старый код: нет таблиц/маршрутов sync) |
|
||||
| antigravity/offline-sync-backend | 5ee7517 | да (предок) | OK | 3/4 | 46/5 | — | — |
|
||||
| codex/finance-calendar-dashboard | c08a7e6 | да (предок) | OK | 4/4 | 48/3 | A4, K4/N1 | CTRL, HARNESS, PILOT-REHOME-SEED, SYNC-L (старый код) |
|
||||
| codex/frontend-library | 331e490 | да (предок) | OK | 3/4 | 46/5 | — | — |
|
||||
| codex/security-finance-production | f74aa6d | да (предок) | OK | 4/4 | 49/2 | — | HARNESS, PILOT-REHOME-SEED, SYNC-L (старый код) |
|
||||
| codex/server-web-production | 2f6b367 | да (предок) | OK | 4/4 | 49/2 | K4/N1 | CTRL, HARNESS, PILOT-REHOME-SEED, SYNC-L (старый код) |
|
||||
|
||||
- `git merge-tree`: **конфликтов нет** ни в одной из 55 пар (Antigravity выстроил цепочку reject → c0,
|
||||
Codex пересобрал format без sync). Шесть веток — предки основы (уже влиты), для них «стало
|
||||
красным» означает только «код старше основы».
|
||||
- `tenant-isolation.test.ts` красный на основе и всех ветках от `f5ac179`
|
||||
(`error: permission denied for table staff_employees`) — это N5, исправление есть в
|
||||
`grok/e2e-harness` (`ba59a48`), в основу ещё не влито.
|
||||
|
||||
### Сборка «как будет после слияния» (локально, не запушено)
|
||||
|
||||
`tmp/combined-round3` = `b0243d5` + merge `codex/fix-live-database` (1573860) + merge
|
||||
`antigravity/fix-sync-c0-bootstrap` (24cc18d) — без конфликтов; `tmp/combined-round3-grok` = то же +
|
||||
merge `grok/e2e-harness`. Вывод — [`round3-combined-merge.md`](report-evidence/round3-combined-merge.md).
|
||||
|
||||
| Сборка | check | integration | e2e red/green | Стало зелёным |
|
||||
| ------------------------------ | -------------------------- | -------------------------- | ------------- | --------------------------------------------- |
|
||||
| combined-round3 (48128c8) | OK (199 passed, 6 skipped) | 6/7, FAIL tenant-isolation | 41/10 | K13/SYNC-A, K13/SYNC-B, K3/N2, K4/N1, SYNC-C0 |
|
||||
| combined-round3-grok (e94415a) | OK (199 passed, 6 skipped) | **7/7** | 41/10 | то же |
|
||||
|
||||
Храповик на `combined-round3-grok`: `[run-e2e] FAIL: K13/SYNC-A, K13/SYNC-B, K3/N2, K4/N1, SYNC-C0 —
|
||||
дефект исправлен, обновите known-defects.json` — ровно список из TASK-GROK-3 п.4. Удалю их отдельным
|
||||
коммитом, когда ветки действительно окажутся в основе.
|
||||
|
||||
### п.5: PILOT-REHOME
|
||||
|
||||
- `[PILOT-REHOME-SEED]` — **зелёный**: 27 таблиц с `tenant_id` из `information_schema.columns`, в каждую
|
||||
2+ строки заглушки `01J00000000000000000000000` и постороннего случайного арендатора (SQL мигратором,
|
||||
`app.tenant_id`/`app.user_id` в транзакции); таблица без сида → красный со списком.
|
||||
- `[PILOT-REHOME]` — **красный** (в `known-defects.json`): `scripts/rehome-tenant.mjs отсутствует
|
||||
(TASK-CODEX-2 п.2.2): … Засеяно таблиц: 27, ledger SUM(amount_minor)=…`.
|
||||
- Сценарий проверен временным эталоном скрипта (не коммитился, удалён): с ним зелёный; мутации
|
||||
«без записи аудита», «без отказа на повтор», «пропущена таблица», «dry-run пишет» — красные
|
||||
([`report-evidence/round3-pilot-rehome.txt`](report-evidence/round3-pilot-rehome.txt)).
|
||||
- Контракт, который проверяет сценарий, — `NOTES-FOR-EXECUTORS.md`, C6.
|
||||
|
||||
## Как запустить
|
||||
|
||||
```sh
|
||||
|
|
@ -139,28 +38,20 @@ export E2E_PG_ADMIN_URL=postgres://<admin>:<pw>@127.0.0.1:55433/postgres # т
|
|||
E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs # храповик known-defects (как в CI)
|
||||
node scripts/test/run-e2e.mjs --no-known-defects # строго: любой красный = exit 1
|
||||
./scripts/test/ci-local.sh # локальный эквивалент verify/format/e2e (bash)
|
||||
node scripts/test/accept-branch.mjs <ветка> [<ветка>…] # приёмка веток (worktree, check, integration, e2e, merge-tree)
|
||||
# роли уже есть (общий кластер): E2E_MIGRATOR_URL=… E2E_APP_URL=… — роли и пароли не трогаются
|
||||
```
|
||||
|
||||
## Таблица по ID (основная ветка f5ac179 + этот стенд)
|
||||
|
||||
Команда: `./scripts/test/ci-local.sh` (внутри `E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs`),
|
||||
2026-09-26 ~16:25 (UTC+7), PostgreSQL 17.11, стенд круга 3:
|
||||
2026-09-26 ~15:28 (UTC+7), PostgreSQL 17.11:
|
||||
|
||||
```text
|
||||
::: [check] with_db_env npm run check
|
||||
Test Files 20 passed (20)
|
||||
Tests 196 passed (196)
|
||||
::: [format-check] FAIL exit=1 # 23 файла базовой линии основы, ни одного из tests/e2e, scripts/test, .forgejo
|
||||
Test Files 6 failed | 1 passed (7)
|
||||
Tests 46 failed | 10 passed (56)
|
||||
== roundtrip OK: rolledBack=11 reapplied=11
|
||||
Test Files 5 failed | 1 passed (6)
|
||||
Tests 45 failed | 9 passed (54)
|
||||
[run-e2e] OK: стенд работает, все красные сценарии — известные открытые дефекты
|
||||
```
|
||||
|
||||
Круг 3 добавил `[PILOT-REHOME-SEED]` (GREEN) и `[PILOT-REHOME]` (RED, `scripts/rehome-tenant.mjs
|
||||
отсутствует (TASK-CODEX-2 п.2.2)`); остальные строки таблицы — без изменений со второго круга.
|
||||
|
||||
| ID владельца / мой | Цвет | Фактический вывод (первая строка assert) |
|
||||
| ---------------------------- | ----- | ------------------------------------------------------------------------------------------------- |
|
||||
| HARNESS (×5), CTRL (×2) | GREEN | 7/7 passed — стенд и позитивные контроли |
|
||||
|
|
@ -202,8 +93,6 @@ node scripts/test/accept-branch.mjs <ветка> [<ветка>…] # приё
|
|||
| SYNC-C0 (нет ID у владельца) | RED | bootstrap pull `HTTP 500 code 42703 column "status" does not exist` |
|
||||
| SYNC-K (нет ID у владельца) | RED | ack курсора из будущего `HTTP 200` |
|
||||
| SYNC-L | GREEN | отозванное устройство получает 403 (позитивный контроль) |
|
||||
| PILOT-REHOME-SEED | GREEN | 27 таблиц с `tenant_id` засеяны (заглушка + посторонний арендатор) |
|
||||
| PILOT-REHOME | RED | `scripts/rehome-tenant.mjs отсутствует (TASK-CODEX-2 п.2.2)` |
|
||||
| PILOT-3 | GREEN | у `app` нет учётных данных мигратора |
|
||||
| N5 (tenant-isolation) | GREEN | в `npm run check` с тестовой БД: `Test Files 20 passed (20)`, `Tests 196 passed (196)` |
|
||||
| N13 (скрипт, не vitest) | — | не воспроизведено: 0 из 30 попыток при 10/25/50 (обход K3 в одноразовой БД), см. ниже |
|
||||
|
|
@ -365,7 +254,3 @@ $ git status --porcelain
|
|||
`codex/fix-live-database`.
|
||||
- S9 проверен удалением SQL-запросом мигратора (публичного API удаления точки нет).
|
||||
- Причина невоспроизводимости N13 — гипотеза по коду.
|
||||
- Круг 3: `scripts/rehome-tenant.mjs` не существует — `[PILOT-REHOME]` проверен только временным
|
||||
эталоном; контракт C6 — предположение. `accept-branch` в CI не запускается (разовый инструмент
|
||||
владельца/приёмщика). Режим `E2E_MIGRATOR_URL/E2E_APP_URL` проверен на том же локальном кластере
|
||||
(роли созданы заранее `provision-pg`), на чужом кластере — не проверено.
|
||||
|
|
|
|||
|
|
@ -71,20 +71,12 @@ export async function dbQuery<T extends pg.QueryResultRow = Record<string, unkno
|
|||
sql: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<T[]> {
|
||||
return dbQueryAs<T>(ctx().tenantId, sql, params);
|
||||
}
|
||||
|
||||
/** Запрос ролью приложения (krona_staff_app) под указанным арендатором (app.tenant_id). */
|
||||
export async function dbQueryAs<T extends pg.QueryResultRow = Record<string, unknown>>(
|
||||
tenantId: string,
|
||||
sql: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<T[]> {
|
||||
const client = new pg.Client({ connectionString: ctx().appDatabaseUrl });
|
||||
const context = ctx();
|
||||
const client = new pg.Client({ connectionString: context.appDatabaseUrl });
|
||||
await client.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
|
||||
await client.query("SELECT set_config('app.tenant_id', $1, true)", [context.tenantId]);
|
||||
const result = await client.query<T>(sql, params);
|
||||
await client.query('COMMIT');
|
||||
return result.rows;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ export interface E2EContext {
|
|||
terminalActorId: string;
|
||||
appDatabaseUrl: string;
|
||||
migrationDatabaseUrl: string;
|
||||
/** Админ (владелец кластера) в БД стенда; null, если не передан. Только для PILOT-REHOME. */
|
||||
adminDatabaseUrl: string | null;
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
|
|
|
|||
|
|
@ -103,47 +103,12 @@ async function startServer(name: string, env: Record<string, string>): Promise<S
|
|||
throw new Error(`server ${name} did not become ready; see ${logPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Откуда брать БД и роли.
|
||||
* 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;
|
||||
} {
|
||||
export default async function setup(project: TestProject) {
|
||||
mkdirSync(evidenceDir, { recursive: true });
|
||||
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_MIGRATOR_URL + E2E_APP_URL',
|
||||
'E2E_PG_ADMIN_URL не задан: нужен bootstrap-админ PostgreSQL 17 (только для создания ролей и БД)',
|
||||
);
|
||||
}
|
||||
const database = process.env.E2E_DATABASE_NAME ?? 'krona_staff_e2e';
|
||||
|
|
@ -156,19 +121,10 @@ function resolveDatabase(): {
|
|||
);
|
||||
const dbEnv = parseEnvFile(envFile);
|
||||
rmSync(secretsDir, { recursive: true, force: true });
|
||||
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),
|
||||
};
|
||||
}
|
||||
const migrationUrl = dbEnv.DATABASE_MIGRATION_URL;
|
||||
const appUrl = dbEnv.DATABASE_URL;
|
||||
if (!migrationUrl || !appUrl) throw new Error('provision did not produce connection URLs');
|
||||
|
||||
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'],
|
||||
|
|
@ -220,7 +176,6 @@ export default async function setup(project: TestProject) {
|
|||
terminalActorId,
|
||||
appDatabaseUrl: appUrl,
|
||||
migrationDatabaseUrl: migrationUrl,
|
||||
adminDatabaseUrl,
|
||||
};
|
||||
project.provide('e2e', context);
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@
|
|||
"K9/D3": "параметры пресета официанта (shift_rate_minor и др.) не сохраняются",
|
||||
"S9": "удаление точки с устройством: 23502 null value in column tenant_id (ON DELETE SET NULL на составном FK)",
|
||||
"A4": "ответы 500 содержат SQLSTATE и текст PostgreSQL",
|
||||
"N12-DATE": "month=2026-13 → 500 (22008) вместо 400",
|
||||
"PILOT-REHOME": "нет scripts/rehome-tenant.mjs (TASK-CODEX-2 п.2.2): перенос заглушки 01J00000000000000000000000 на настоящий tenant не реализован"
|
||||
"N12-DATE": "month=2026-13 → 500 (22008) вместо 400"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
# Команда: node scripts/test/accept-branch.mjs $(все origin/codex/* и origin/antigravity/* кроме основы) — после git fetch в 16:16 NOVT
|
||||
|
||||
## accept-branch — 2026-09-26 09:20 UTC
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): 6f408bc; БД: роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep. Логи: `tests/e2e/evidence/accept/2026-09-26T09-16-26/`.
|
||||
|
||||
### Сводка
|
||||
|
||||
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
|
||||
| ------------------------------------------------- | ------- | -------------- | --------------------------- | ------------------------------------------- | ------------------ | ------------------------------- |
|
||||
| origin/codex/bootstrap-platform-foundation (база) | b0243d5 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | n/a | n/a |
|
||||
| origin/antigravity/employee-notifications | 3e20544 | OK (164 passed | 1 skipped (165)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 49/2 | — | HARNESS (AssertionError: expected 10 to be greater than or equal to 11); PILOT-REHOME-SEED (error: relation "staff_sync_change_log" does not exist); SYNC-L (AssertionError: HTTP 404 {"message":"Route POST:/api/v1/staff/sync/devices not found","error":"Not Found","statusCode":404}: expected 404 to be 201 // Object.is) |
|
||||
| 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/antigravity/offline-sync-backend | 5ee7517 | OK (192 passed | 1 skipped (193)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | — | — |
|
||||
| origin/codex/finance-calendar-dashboard | c08a7e6 | OK (113 passed | 1 skipped (114)) | 4/4 зелёных | 48/3 | A4, K4/N1 | CTRL (AssertionError: expected 200 to be 401 // Object.is equality); HARNESS (AssertionError: expected 8 to be greater than or equal to 11); PILOT-REHOME-SEED (error: relation "staff_professions" does not exist); SYNC-L (AssertionError: HTTP 404 {"message":"Route POST:/api/v1/staff/sync/devices not found","error":"Not Found","statusCode":404}: expected 404 to be 201 // Object.is) |
|
||||
| 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/frontend-library | 331e490 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | — | — |
|
||||
| origin/codex/security-finance-production | f74aa6d | OK (142 passed | 1 skipped (143)) | 4/4 зелёных | 49/2 | — | HARNESS (AssertionError: expected 9 to be greater than or equal to 11); PILOT-REHOME-SEED (error: relation "staff_push_subscriptions" does not exist); SYNC-L (AssertionError: HTTP 404 {"message":"Route POST:/api/v1/staff/sync/devices not found","error":"Not Found","statusCode":404}: expected 404 to be 201 // Object.is) |
|
||||
| origin/codex/server-web-production | 2f6b367 | OK (113 passed | 1 skipped (114)) | 4/4 зелёных | 49/2 | K4/N1 | CTRL (AssertionError: HTTP 404 {"message":"Route POST:/api/v1/staff/imports/owner-workbook not found","error":"Not Found","statusCode":404}: expected 404 to be less t); HARNESS (AssertionError: expected 7 to be greater than or equal to 11); PILOT-REHOME-SEED (error: relation "staff_professions" does not exist); SYNC-L (AssertionError: HTTP 404 {"message":"Route POST:/api/v1/staff/sync/devices not found","error":"Not Found","statusCode":404}: expected 404 to be 201 // Object.is) |
|
||||
|
||||
### Интеграционные тесты (каждый файл отдельно, свежая БД)
|
||||
|
||||
| Ветка | Файл | Итог | 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/employee-notifications | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/antigravity/employee-notifications | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/antigravity/employee-notifications | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| origin/antigravity/employee-notifications | 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/antigravity/offline-sync-backend | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/antigravity/offline-sync-backend | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/antigravity/offline-sync-backend | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| origin/antigravity/offline-sync-backend | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
|
||||
| origin/codex/finance-calendar-dashboard | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/codex/finance-calendar-dashboard | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/codex/finance-calendar-dashboard | portal.test.ts | OK | 1 passed (1) | 11 passed (11) | |
|
||||
| origin/codex/finance-calendar-dashboard | tenant-isolation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| 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/frontend-library | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/codex/frontend-library | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/codex/frontend-library | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| origin/codex/frontend-library | tenant-isolation.test.ts | FAIL | 1 failed (1) | 1 passed (1) | error: permission denied for table staff_employees |
|
||||
| origin/codex/security-finance-production | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/codex/security-finance-production | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/codex/security-finance-production | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| origin/codex/security-finance-production | tenant-isolation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/codex/server-web-production | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/codex/server-web-production | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| origin/codex/server-web-production | portal.test.ts | OK | 1 passed (1) | 11 passed (11) | |
|
||||
| origin/codex/server-web-production | tenant-isolation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
|
||||
npm ci (lock отличается): origin/codex/finance-calendar-dashboard, origin/codex/security-finance-production, origin/codex/server-web-production
|
||||
|
||||
### Попарные слияния (`git merge-tree --write-tree`)
|
||||
|
||||
| Ветка A | Ветка B | Результат | Конфликтные файлы |
|
||||
| ------------------------------------------- | ------------------------------------------- | --------- | ----------------- |
|
||||
| origin/codex/bootstrap-platform-foundation | origin/antigravity/employee-notifications | чисто | |
|
||||
| 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/antigravity/offline-sync-backend | чисто | |
|
||||
| origin/codex/bootstrap-platform-foundation | origin/codex/finance-calendar-dashboard | чисто | |
|
||||
| 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/frontend-library | чисто | |
|
||||
| origin/codex/bootstrap-platform-foundation | origin/codex/security-finance-production | чисто | |
|
||||
| origin/codex/bootstrap-platform-foundation | origin/codex/server-web-production | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/antigravity/fix-reject-device-shifts | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/antigravity/fix-sync-c0-bootstrap | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/antigravity/offline-sync-backend | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/codex/finance-calendar-dashboard | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/codex/fix-format-baseline | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/codex/fix-live-database | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/codex/frontend-library | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/codex/security-finance-production | чисто | |
|
||||
| origin/antigravity/employee-notifications | origin/codex/server-web-production | чисто | |
|
||||
| origin/antigravity/fix-reject-device-shifts | origin/antigravity/fix-sync-c0-bootstrap | чисто | |
|
||||
| origin/antigravity/fix-reject-device-shifts | origin/antigravity/offline-sync-backend | чисто | |
|
||||
| origin/antigravity/fix-reject-device-shifts | origin/codex/finance-calendar-dashboard | чисто | |
|
||||
| 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/frontend-library | чисто | |
|
||||
| origin/antigravity/fix-reject-device-shifts | origin/codex/security-finance-production | чисто | |
|
||||
| origin/antigravity/fix-reject-device-shifts | origin/codex/server-web-production | чисто | |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | origin/antigravity/offline-sync-backend | чисто | |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/finance-calendar-dashboard | чисто | |
|
||||
| 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/frontend-library | чисто | |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/security-finance-production | чисто | |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/server-web-production | чисто | |
|
||||
| origin/antigravity/offline-sync-backend | origin/codex/finance-calendar-dashboard | чисто | |
|
||||
| origin/antigravity/offline-sync-backend | origin/codex/fix-format-baseline | чисто | |
|
||||
| origin/antigravity/offline-sync-backend | origin/codex/fix-live-database | чисто | |
|
||||
| origin/antigravity/offline-sync-backend | origin/codex/frontend-library | чисто | |
|
||||
| origin/antigravity/offline-sync-backend | origin/codex/security-finance-production | чисто | |
|
||||
| origin/antigravity/offline-sync-backend | origin/codex/server-web-production | чисто | |
|
||||
| origin/codex/finance-calendar-dashboard | origin/codex/fix-format-baseline | чисто | |
|
||||
| origin/codex/finance-calendar-dashboard | origin/codex/fix-live-database | чисто | |
|
||||
| origin/codex/finance-calendar-dashboard | origin/codex/frontend-library | чисто | |
|
||||
| origin/codex/finance-calendar-dashboard | origin/codex/security-finance-production | чисто | |
|
||||
| origin/codex/finance-calendar-dashboard | origin/codex/server-web-production | чисто | |
|
||||
| origin/codex/fix-format-baseline | origin/codex/fix-live-database | чисто | |
|
||||
| origin/codex/fix-format-baseline | origin/codex/frontend-library | чисто | |
|
||||
| origin/codex/fix-format-baseline | origin/codex/security-finance-production | чисто | |
|
||||
| origin/codex/fix-format-baseline | origin/codex/server-web-production | чисто | |
|
||||
| origin/codex/fix-live-database | origin/codex/frontend-library | чисто | |
|
||||
| origin/codex/fix-live-database | origin/codex/security-finance-production | чисто | |
|
||||
| origin/codex/fix-live-database | origin/codex/server-web-production | чисто | |
|
||||
| origin/codex/frontend-library | origin/codex/security-finance-production | чисто | |
|
||||
| origin/codex/frontend-library | origin/codex/server-web-production | чисто | |
|
||||
| origin/codex/security-finance-production | origin/codex/server-web-production | чисто | |
|
||||
|
||||
### Форматирование без поведения
|
||||
|
||||
**origin/codex/fix-format-baseline** (a05777c), merge-base f5ac179, изменено файлов: 19 — ТОЛЬКО ФОРМАТИРОВАНИЕ
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# Команда: node scripts/test/accept-branch.mjs antigravity/fix-reject-device-shifts antigravity/fix-sync-c0-bootstrap codex/fix-format-baseline (16:03 NOVT; ветка Antigravity к этому времени уже 6c5bf36; версия скрипта до вердикта SKIP)
|
||||
|
||||
## accept-branch — 2026-09-26 09:04 UTC
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): aa7f5f8 (+ незакоммиченные изменения стенда); БД: роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep. Логи: `tests/e2e/evidence/accept/2026-09-26T09-03-17/`.
|
||||
|
||||
### Сводка
|
||||
|
||||
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
|
||||
| ------------------------------------------------- | ------- | -------------- | --------------------------- | ------------------------------------------------------------------- | ------------------ | ---------------------- |
|
||||
| origin/codex/bootstrap-platform-foundation (база) | b0243d5 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 45/4 | n/a | n/a |
|
||||
| origin/antigravity/fix-reject-device-shifts | 6c5bf36 | OK (196 passed | 3 skipped (199)) | 4/5 зелёных; FAIL: tenant-isolation.test.ts | 43/6 | K13/SYNC-A, K13/SYNC-B | — |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | 8baad55 | OK (196 passed | 3 skipped (199)) | 3/5 зелёных; FAIL: sync-bootstrap.test.ts, tenant-isolation.test.ts | 44/5 | SYNC-C0 | — |
|
||||
| origin/codex/fix-format-baseline | 513cdff | OK (196 passed | 1 skipped (197)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 43/6 | K13/SYNC-A, K13/SYNC-B | — |
|
||||
|
||||
### Интеграционные тесты (каждый файл отдельно, свежая БД)
|
||||
|
||||
| Ветка | Файл | Итог | 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 | FAIL | 1 failed (1) | 2 passed (2) | Error: Called end on pool more than once |
|
||||
| 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 |
|
||||
|
||||
### Попарные слияния (`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/antigravity/fix-reject-device-shifts | origin/antigravity/fix-sync-c0-bootstrap | КОНФЛИКТ | src/domain/sync/service.ts |
|
||||
| origin/antigravity/fix-reject-device-shifts | origin/codex/fix-format-baseline | чисто | |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-format-baseline | КОНФЛИКТ | src/domain/sync/service.ts |
|
||||
|
||||
### Форматирование без поведения
|
||||
|
||||
**origin/codex/fix-format-baseline** (513cdff), merge-base f5ac179, изменено файлов: 24 — ЕСТЬ ИЗМЕНЕНИЯ СВЕРХ PRETTIER
|
||||
|
||||
| Файл | Почему | Строк сверх prettier | Эти же строки в ветке |
|
||||
| -------------------------- | ----------------------------- | -------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| src/domain/sync/service.ts | не совпадает с prettier(база) | 232 | antigravity/fix-reject-device-shifts: 232/232; antigravity/fix-sync-c0-bootstrap: 6/232 |
|
||||
| tests/unit/sync.test.ts | не совпадает с prettier(база) | 130 | antigravity/fix-reject-device-shifts: 130/130; antigravity/fix-sync-c0-bootstrap: 12/130 |
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# Команда: node scripts/test/accept-branch.mjs 1df2019 antigravity/fix-sync-c0-bootstrap codex/fix-format-baseline (SHA 1df2019 = прежняя голова antigravity/fix-reject-device-shifts; 8baad55, 513cdff — головы на момент прогона, 16:04 NOVT)
|
||||
|
||||
## accept-branch — 2026-09-26 09:06 UTC
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): aa7f5f8 (+ незакоммиченные изменения стенда); БД: роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep. Логи: `tests/e2e/evidence/accept/2026-09-26T09-05-12/`.
|
||||
|
||||
### Сводка
|
||||
|
||||
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
|
||||
| ------------------------------------------------- | ------- | -------------- | --------------------------- | ------------------------------------------------------------------- | ------------------ | ---------------------- |
|
||||
| origin/codex/bootstrap-platform-foundation (база) | b0243d5 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 45/4 | n/a | n/a |
|
||||
| 1df2019 | 1df2019 | OK (196 passed | 3 skipped (199)) | 3/5 зелёных; FAIL: sync-rejection.test.ts, tenant-isolation.test.ts | 43/6 | K13/SYNC-A, K13/SYNC-B | — |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | 8baad55 | OK (196 passed | 3 skipped (199)) | 3/5 зелёных; FAIL: sync-bootstrap.test.ts, tenant-isolation.test.ts | 44/5 | SYNC-C0 | — |
|
||||
| origin/codex/fix-format-baseline | 513cdff | OK (196 passed | 1 skipped (197)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 43/6 | K13/SYNC-A, K13/SYNC-B | — |
|
||||
|
||||
### Интеграционные тесты (каждый файл отдельно, свежая БД)
|
||||
|
||||
| Ветка | Файл | Итог | 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 |
|
||||
| 1df2019 | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| 1df2019 | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| 1df2019 | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| 1df2019 | sync-rejection.test.ts | FAIL | 1 failed (1) | 2 skipped (2) | error: column "created_by" of relation "staff_employees" does not exist |
|
||||
| 1df2019 | 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 | FAIL | 1 failed (1) | 2 passed (2) | Error: Called end on pool more than once |
|
||||
| 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 |
|
||||
|
||||
### Попарные слияния (`git merge-tree --write-tree`)
|
||||
|
||||
| Ветка A | Ветка B | Результат | Конфликтные файлы |
|
||||
| ------------------------------------------ | ---------------------------------------- | --------- | -------------------------- |
|
||||
| origin/codex/bootstrap-platform-foundation | 1df2019 | чисто | |
|
||||
| origin/codex/bootstrap-platform-foundation | origin/antigravity/fix-sync-c0-bootstrap | чисто | |
|
||||
| origin/codex/bootstrap-platform-foundation | origin/codex/fix-format-baseline | чисто | |
|
||||
| 1df2019 | origin/antigravity/fix-sync-c0-bootstrap | КОНФЛИКТ | src/domain/sync/service.ts |
|
||||
| 1df2019 | origin/codex/fix-format-baseline | чисто | |
|
||||
| origin/antigravity/fix-sync-c0-bootstrap | origin/codex/fix-format-baseline | КОНФЛИКТ | src/domain/sync/service.ts |
|
||||
|
||||
### Форматирование без поведения
|
||||
|
||||
**origin/codex/fix-format-baseline** (513cdff), merge-base f5ac179, изменено файлов: 24 — ЕСТЬ ИЗМЕНЕНИЯ СВЕРХ PRETTIER
|
||||
|
||||
| Файл | Почему | Строк сверх prettier | Эти же строки в ветке |
|
||||
| -------------------------- | ----------------------------- | -------------------- | ----------------------------------------------------------- |
|
||||
| src/domain/sync/service.ts | не совпадает с prettier(база) | 232 | 1df2019: 232/232; antigravity/fix-sync-c0-bootstrap: 6/232 |
|
||||
| tests/unit/sync.test.ts | не совпадает с prettier(база) | 130 | 1df2019: 130/130; antigravity/fix-sync-c0-bootstrap: 12/130 |
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# Локальные ветки (не запушены): tmp/combined-round3 = b0243d5 + merge origin/codex/fix-live-database (1573860) + merge origin/antigravity/fix-sync-c0-bootstrap (24cc18d), без конфликтов; tmp/combined-round3-grok = то же + merge grok/e2e-harness (6f408bc). Команда: node scripts/test/accept-branch.mjs <ветка>
|
||||
|
||||
## accept-branch — 2026-09-26 09:22 UTC
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): 6f408bc; БД: роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep. Логи: `tests/e2e/evidence/accept/2026-09-26T09-22-01/`.
|
||||
|
||||
### Сводка
|
||||
|
||||
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
|
||||
| ------------------------------------------------- | ------- | -------------- | --------------------------- | ------------------------------------------- | ------------------ | --------------------------------------------- |
|
||||
| origin/codex/bootstrap-platform-foundation (база) | b0243d5 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | n/a | n/a |
|
||||
| tmp/combined-round3 | 48128c8 | OK (199 passed | 6 skipped (205)) | 6/7 зелёных; FAIL: tenant-isolation.test.ts | 41/10 | K13/SYNC-A, K13/SYNC-B, K3/N2, K4/N1, SYNC-C0 | — |
|
||||
|
||||
### Интеграционные тесты (каждый файл отдельно, свежая БД)
|
||||
|
||||
| Ветка | Файл | Итог | 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 |
|
||||
| tmp/combined-round3 | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| tmp/combined-round3 | live-postgres-remediation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| tmp/combined-round3 | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| tmp/combined-round3 | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| tmp/combined-round3 | sync-bootstrap.test.ts | OK | 1 passed (1) | 2 passed (2) | |
|
||||
| tmp/combined-round3 | sync-rejection.test.ts | OK | 1 passed (1) | 2 passed (2) | |
|
||||
| tmp/combined-round3 | 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 | tmp/combined-round3 | чисто | |
|
||||
|
||||
---
|
||||
|
||||
## accept-branch — 2026-09-26 09:23 UTC
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): 6f408bc; БД: роли созданы один раз через E2E_PG_ADMIN_URL (provision-pg), дальше --roles keep. Логи: `tests/e2e/evidence/accept/2026-09-26T09-23-04/`.
|
||||
|
||||
### Сводка
|
||||
|
||||
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
|
||||
| ------------------------------------------------- | ------- | -------------- | --------------------------- | ------------------------------------------- | ------------------ | --------------------------------------------- |
|
||||
| origin/codex/bootstrap-platform-foundation (база) | b0243d5 | OK (195 passed | 1 skipped (196)) | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | n/a | n/a |
|
||||
| tmp/combined-round3-grok | e94415a | OK (199 passed | 6 skipped (205)) | 7/7 зелёных | 41/10 | K13/SYNC-A, K13/SYNC-B, K3/N2, K4/N1, SYNC-C0 | — |
|
||||
|
||||
### Интеграционные тесты (каждый файл отдельно, свежая БД)
|
||||
|
||||
| Ветка | Файл | Итог | 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 |
|
||||
| tmp/combined-round3-grok | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| tmp/combined-round3-grok | live-postgres-remediation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| tmp/combined-round3-grok | openapi-runtime.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
| tmp/combined-round3-grok | portal.test.ts | OK | 1 passed (1) | 12 passed (12) | |
|
||||
| tmp/combined-round3-grok | sync-bootstrap.test.ts | OK | 1 passed (1) | 2 passed (2) | |
|
||||
| tmp/combined-round3-grok | sync-rejection.test.ts | OK | 1 passed (1) | 2 passed (2) | |
|
||||
| tmp/combined-round3-grok | tenant-isolation.test.ts | OK | 1 passed (1) | 1 passed (1) | |
|
||||
|
||||
Внимание: ветка меняла файлы стенда, при приёмке они заменены оверлеем:
|
||||
|
||||
- tmp/combined-round3-grok: scripts/test/accept-branch.mjs, scripts/test/ci-local.sh, scripts/test/migrate-roundtrip.mjs, scripts/test/n13-race.mjs, scripts/test/node-commands.d.mts, scripts/test/node-commands.mjs, scripts/test/provision-pg.mjs, scripts/test/run-e2e.mjs, tests/e2e/00-harness.e2e.ts, tests/e2e/10-core-defects.e2e.ts, tests/e2e/20-sync-defects.e2e.ts, tests/e2e/30-owner-decisions.e2e.ts, tests/e2e/40-compose-pilot.e2e.ts, tests/e2e/50-money-and-errors.e2e.ts, tests/e2e/60-pilot-rehome.e2e.ts, tests/e2e/NOTES-FOR-EXECUTORS.md, tests/e2e/REPORT.md, tests/e2e/harness/client.ts, tests/e2e/harness/context.ts, tests/e2e/harness/fixtures.ts, tests/e2e/harness/global-setup.ts, tests/e2e/harness/node-commands.test.ts, tests/e2e/known-defects.json, tests/e2e/report-evidence/acceptance-antigravity-fix-reject-device-shifts.txt, tests/e2e/report-evidence/acceptance-codex-fix-format-baseline.txt, tests/e2e/report-evidence/acceptance-codex-fix-live-database.txt, tests/e2e/report-evidence/n13-race.txt, tests/e2e/report-evidence/n5-after.txt, tests/e2e/report-evidence/n5-before.txt, tests/e2e/report-evidence/round3-pilot-rehome.txt, tests/e2e/vitest.e2e.config.ts
|
||||
|
||||
### Попарные слияния (`git merge-tree --write-tree`)
|
||||
|
||||
| Ветка A | Ветка B | Результат | Конфликтные файлы |
|
||||
| ------------------------------------------ | ------------------------ | --------- | ----------------- |
|
||||
| origin/codex/bootstrap-platform-foundation | tmp/combined-round3-grok | чисто | |
|
||||
|
||||
```
|
||||
[run-e2e] FAIL:
|
||||
- K13/SYNC-A: дефект исправлен, обновите known-defects.json
|
||||
- K13/SYNC-B: дефект исправлен, обновите known-defects.json
|
||||
- K3/N2: дефект исправлен, обновите known-defects.json
|
||||
- K4/N1: дефект исправлен, обновите known-defects.json
|
||||
- SYNC-C0: дефект исправлен, обновите known-defects.json
|
||||
```
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# Команда: node scripts/test/accept-branch.mjs --format-check tmp/prettier-only-f5ac179 --format-check codex/fix-format-baseline (tmp/prettier-only-f5ac179 = c552d38: `prettier --write .` поверх f5ac179, локальная ветка, не запушена; 513cdff — прежняя голова codex/fix-format-baseline)
|
||||
|
||||
[accept] tmp/prettier-only-f5ac179: проверка форматирования
|
||||
[accept] codex/fix-format-baseline: проверка форматирования
|
||||
|
||||
## accept-branch — 2026-09-26 09:03 UTC
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): aa7f5f8 (+ незакоммиченные изменения стенда); БД: не использовалась. Логи: `tests/e2e/evidence/accept/2026-09-26T09-03-11/`.
|
||||
|
||||
### Форматирование без поведения
|
||||
|
||||
**tmp/prettier-only-f5ac179** (c552d38), merge-base f5ac179, изменено файлов: 24 — ТОЛЬКО ФОРМАТИРОВАНИЕ
|
||||
|
||||
**origin/codex/fix-format-baseline** (513cdff), merge-base f5ac179, изменено файлов: 24 — ЕСТЬ ИЗМЕНЕНИЯ СВЕРХ PRETTIER
|
||||
|
||||
| Файл | Почему | Строк сверх prettier | Эти же строки в ветке |
|
||||
| -------------------------- | ----------------------------- | -------------------- | --------------------- |
|
||||
| src/domain/sync/service.ts | не совпадает с prettier(база) | 232 | — |
|
||||
| tests/unit/sync.test.ts | не совпадает с prettier(база) | 130 | — |
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# PILOT-REHOME: доказательство, что сценарий ловит ошибки переноса (TASK-GROK-3 п.5)
|
||||
# Эталон — временный scripts/rehome-tenant.mjs (минимальный UPDATE tenant_id под session_replication_role=replica + запись аудита);
|
||||
# он НЕ коммитился и удалён после прогона. Команда: node scripts/test/run-e2e.mjs (PG17, admin-режим).
|
||||
|
||||
== эталон (reference), не коммитится
|
||||
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 176ms
|
||||
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 340ms
|
||||
× tests/e2e/30-owner-decisions.e2e.ts > Решения владельца: роли, пакеты зарплаты, согласие на биометрию > [D6/OWN-01] менеджер точки A запускает расчёт для точки B → 403 111ms
|
||||
== эталон (no-audit), не коммитится
|
||||
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 164ms
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 191ms
|
||||
→ staff_audit_log: у нового арендатора нет новой записи о переносе: expected 2 to be greater than 2
|
||||
FAIL tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает
|
||||
== эталон (dry-writes), не коммитится
|
||||
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 182ms
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 88ms
|
||||
→ --dry-run изменил данные заглушки: expected { counts: { …(27) }, …(1) } to deeply equal { counts: { …(27) }, …(1) }
|
||||
FAIL tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает
|
||||
== без скрипта
|
||||
✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 169ms
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 4ms
|
||||
→ scripts/rehome-tenant.mjs отсутствует (TASK-CODEX-2 п.2.2): перенос заглушки 01J00000000000000000000000 → 01M2R6AC5652NPT3MG34DKN4ZP не реализован; контракт сценария — NOTES-FOR-EXECUTORS.md C6. Засеяно таблиц: 27, ledger SUM(amount_minor)=313227
|
||||
FAIL tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает
|
||||
|
||||
# Мутации эталона (первый прогон, до уточнения сообщений ассертов)
|
||||
== мутация эталона: no-audit
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 196ms
|
||||
→ expected 2 to be greater than 2
|
||||
== мутация эталона: no-refuse
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 319ms
|
||||
→ повторный перенос должен отказать: expected +0 not to be +0 // Object.is equality
|
||||
== мутация эталона: skip-table
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 195ms
|
||||
→ под заглушкой остались строки: expected [ 'staff_work_items' ] to deeply equal []
|
||||
== мутация эталона: dry-writes
|
||||
× tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 91ms
|
||||
→ expected { counts: { …(27) }, …(1) } to deeply equal { counts: { …(27) }, …(1) }
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Режим готовых ролей: E2E_MIGRATOR_URL/E2E_APP_URL заданы, E2E_PG_ADMIN_URL не задан. Команда: node scripts/test/accept-branch.mjs --skip-check codex/fix-live-database (8e955b0)
|
||||
|
||||
База: `origin/codex/bootstrap-platform-foundation` = b0243d5; стенд (оверлей tests/e2e, scripts/test): d7b2352 (+ незакоммиченные изменения стенда); БД: роли из E2E_MIGRATOR_URL/E2E_APP_URL (не пересоздавались). Логи: `tests/e2e/evidence/accept/2026-09-26T09-15-44/`.
|
||||
|
||||
### Сводка
|
||||
|
||||
| Ветка | SHA | npm run check | integration (красные файлы) | e2e red/green | e2e: стало зелёным | e2e: стало красным |
|
||||
| ------------------------------------------------- | ------- | ------------- | ------------------------------------------------------------------------------------------------------------ | ------------- | ------------------------------------ | ------------------ |
|
||||
| origin/codex/bootstrap-platform-foundation (база) | b0243d5 | не запускался | 3/4 зелёных; FAIL: tenant-isolation.test.ts | 46/5 | n/a | n/a |
|
||||
| origin/codex/fix-live-database | 8e955b0 | не запускался | 3/5 зелёных; FAIL: tenant-isolation.test.ts; SKIP (файл пропущен целиком): live-postgres-remediation.test.ts | 42/9 | K13/SYNC-A, K13/SYNC-B, K3/N2, K4/N1 | — |
|
||||
|
||||
### Интеграционные тесты (каждый файл отдельно, свежая БД)
|
||||
|
||||
| Ветка | Файл | Итог | 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/codex/fix-live-database | health.test.ts | OK | 1 passed (1) | 3 passed (3) | |
|
||||
| origin/codex/fix-live-database | live-postgres-remediation.test.ts | SKIP | 1 skipped (1) | 1 skipped (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 |
|
||||
|
||||
### Попарные слияния (`git merge-tree --write-tree`)
|
||||
Loading…
Reference in a new issue