krona-staff/scripts/test/run-e2e.mjs
grok fc20f75b55 test(e2e): evidence прогонов вне git, в CI — артефакт
Зачем: tests/e2e/evidence/ перезаписывается каждым прогоном (логи серверов
с ULID и портами, JSON vitest), поэтому любой запуск стенда менял
отслеживаемые файлы и засорял дифф (приёмка владельца, TASK-GROK-2 п.1).

Что: tests/e2e/evidence/ убран из индекса (git rm --cached) и добавлен в
корневой .gitignore (его же читает prettier, поэтому форматировать
сгенерированные файлы больше не нужно — вызов prettier из run-e2e убран).
В CI задача e2e выгружает папку артефактом e2e-evidence
(actions/upload-artifact, закреплён SHA v4.6.2; Forgejo 9.0.3 поддерживает
v4). Разовые доказательства, которые скрипты не перезаписывают (N5 до/после,
замер N13), перенесены в tests/e2e/report-evidence/ — на них ссылается
REPORT.md.
2026-09-26 15:20:28 +07:00

124 lines
5.8 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.
// tests/e2e/evidence/ не хранится в git (.gitignore): в CI это артефакт задачи e2e.
import { spawn } from 'node:child_process';
import { createWriteStream, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { nodeTool } from './node-commands.mjs';
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 = [
'run',
'--config',
'tests/e2e/vitest.e2e.config.ts',
'--reporter=verbose',
'--reporter=json',
`--outputFile.json=${jsonPath}`,
...extra,
];
const log = createWriteStream(logPath);
const vitest = nodeTool('vitest', vitestArgs);
log.write(`$ node <vitest.mjs> ${vitestArgs.join(' ')}\n`);
const exitCode = await new Promise((resolve) => {
const child = spawn(vitest.command, vitest.args, { 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`);
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: стенд работает, все красные сценарии — известные открытые дефекты');