krona-staff/scripts/test/run-e2e.mjs
grok c68370b890 test(e2e): форматировать сгенерированные json/md evidence prettier
Зачем: tests/e2e/evidence не входит в .prettierignore (файл вне разрешённых путей), и минифицированный JSON vitest делал бы npm run format:check красным из-за артефактов, а не из-за кода.
2026-09-26 15:18:31 +07:00

128 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Запуск e2e с «храповиком» известных дефектов.
// * Сценарии проверяют ПРАВИЛЬНОЕ поведение, поэтому открытые дефекты дают красные тесты.
// * tests/e2e/known-defects.json — список ID, красный цвет которых сейчас ожидаем.
// * exit 0, если: все [HARNESS]-тесты зелёные И каждый красный тест есть в списке
// И ни один ID из списка не стал полностью зелёным (исправленный дефект нужно
// убрать из списка — иначе регрессию потом никто не заметит).
// * --no-known-defects: строгий режим, exit = код vitest (все дефекты красные).
// Полный вывод пишется в tests/e2e/evidence/e2e-run.txt, таблица — в e2e-table.md.
import { spawn, spawnSync } from 'node:child_process';
import { createWriteStream, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
const repoRoot = path.resolve(import.meta.dirname, '../..');
const evidence = path.join(repoRoot, 'tests/e2e/evidence');
mkdirSync(evidence, { recursive: true });
const strict = process.argv.includes('--no-known-defects');
const jsonPath = path.join(evidence, 'e2e-results.json');
const logPath = path.join(evidence, 'e2e-run.txt');
const extra = process.argv.slice(2).filter((arg) => !arg.startsWith('--no-known-defects'));
const vitestArgs = [
'vitest',
'run',
'--config',
'tests/e2e/vitest.e2e.config.ts',
'--reporter=verbose',
'--reporter=json',
`--outputFile.json=${jsonPath}`,
...extra,
];
const log = createWriteStream(logPath);
log.write(`$ npx ${vitestArgs.join(' ')}\n`);
const exitCode = await new Promise((resolve) => {
const child = spawn('npx', vitestArgs, { cwd: repoRoot, env: process.env });
for (const stream of [child.stdout, child.stderr]) {
stream.on('data', (chunk) => {
process.stdout.write(chunk);
log.write(chunk);
});
}
child.on('close', (code) => resolve(code ?? 1));
});
log.end();
let results;
try {
results = JSON.parse(readFileSync(jsonPath, 'utf8'));
} catch {
console.error('[run-e2e] нет JSON-результатов: стенд не поднялся (см. e2e-run.txt)');
process.exit(1);
}
const known = strict
? {}
: JSON.parse(readFileSync(path.join(repoRoot, 'tests/e2e/known-defects.json'), 'utf8')).defects;
const byId = new Map();
for (const file of results.testResults) {
for (const test of file.assertionResults) {
const id = /^\[([A-Z0-9-]+)\]/.exec(test.title)?.[1] ?? 'UNTAGGED';
const entry = byId.get(id) ?? { passed: 0, failed: 0, tests: [] };
if (test.status === 'passed') entry.passed += 1;
else if (test.status === 'failed') entry.failed += 1;
entry.tests.push({ title: test.title, status: test.status });
byId.set(id, entry);
}
}
const problems = [];
// Страховка: в evidence не должно попасть ничего похожего на секреты стенда
// (hex-токены 48/64 символа, токены устройств kstaff_dev_*, URL с паролем).
const secretPatterns = [
/\b[0-9a-f]{48}\b/,
/\b[0-9a-f]{64}\b/,
/kstaff_dev_[0-9A-Z]{26}_[0-9a-f]{16,}/,
/postgres:\/\/[^\s:@]+:[^\s@]+@/,
];
for (const name of readdirSync(evidence)) {
const content = readFileSync(path.join(evidence, name), 'utf8');
for (const pattern of secretPatterns) {
if (pattern.test(content))
problems.push(`evidence/${name}: найдено похожее на секрет (${pattern})`);
}
}
const rows = [];
for (const [id, entry] of [...byId.entries()].sort()) {
const red = entry.failed > 0;
let verdict;
if (id === 'HARNESS' || id === 'UNTAGGED') {
verdict = red ? 'СТЕНД СЛОМАН' : 'зелёный (позитивный контроль)';
if (red || id === 'UNTAGGED') problems.push(`${id}: ${red ? 'красный' : 'тест без ID'}`);
} else if (red && known[id]) verdict = 'красный (известный дефект)';
else if (red) {
verdict = 'красный (НЕ в known-defects)';
problems.push(`${id}: новый красный тест — регрессия или новый дефект`);
} else if (known[id]) {
verdict = 'зелёный — ИСПРАВЛЕН, уберите из known-defects.json';
problems.push(`${id}: дефект исправлен, обновите known-defects.json`);
} else verdict = 'зелёный';
rows.push(
`| ${id} | ${red ? 'RED' : 'GREEN'} | ${entry.passed}/${entry.passed + entry.failed} | ${verdict} |`,
);
}
for (const id of Object.keys(known)) {
if (!byId.has(id)) problems.push(`${id}: есть в known-defects.json, но сценария нет`);
}
const table = ['| ID | Цвет | Зелёных/всего | Вердикт |', '|---|---|---|---|', ...rows].join('\n');
writeFileSync(path.join(evidence, 'e2e-table.md'), `${table}\n`);
// Сгенерированные json/md приводятся к prettier, чтобы `npm run format:check` не
// краснел из-за evidence (tests/e2e/evidence не в .prettierignore).
spawnSync('npx', ['prettier', '--write', jsonPath, path.join(evidence, 'e2e-table.md')], {
cwd: repoRoot,
stdio: 'ignore',
});
console.log(`\n[run-e2e] vitest exit=${exitCode}\n${table}`);
if (strict) {
if (problems.some((p) => p.startsWith('evidence/'))) {
console.error(`\n[run-e2e] FAIL:\n- ${problems.join('\n- ')}`);
process.exit(1);
}
process.exit(exitCode);
}
if (problems.length > 0) {
console.error(`\n[run-e2e] FAIL:\n- ${problems.join('\n- ')}`);
process.exit(1);
}
console.log('\n[run-e2e] OK: стенд работает, все красные сценарии — известные открытые дефекты');