#!/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 ${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: стенд работает, все красные сценарии — известные открытые дефекты');