Files
ECC/tests/run-all.js
T
haelyra aac9b3d34b fix: preserve activity errors, asset bytes and runner summaries
Treat non-2xx activity responses as feed errors, preserve non-Markdown Copilot agent assets, and apply the reviewed reference escape parity correction with regenerated bindings. Allow the test runner to drain piped output on its existing success or failure exit status. Validate installed workflows and bounded real Node20/24 pipe regressions.
2026-10-09 19:33:37 -04:00

213 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const testsDir = __dirname;
const repoRoot = path.resolve(testsDir, '..');
const TEST_GLOB = 'tests/**/*.test.js';
const filter = (((process.argv || [])[2]) || '').split(path.sep).join('/');
const skipPatterns = (process.env.ECC_TEST_SKIP || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
function matchesTestGlob(relativePath) {
const normalized = relativePath.split(path.sep).join('/');
if (filter && !normalized.includes(filter)) {
return false;
}
if (skipPatterns.some(pattern => normalized.includes(pattern))) {
return false;
}
if (typeof path.matchesGlob === 'function') {
return path.matchesGlob(normalized, TEST_GLOB);
}
return /^tests\/(?:.+\/)?[^/]+\.test\.js$/.test(normalized);
}
function walkFiles(dir, acc = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkFiles(fullPath, acc);
} else if (entry.isFile()) {
acc.push(fullPath);
}
}
return acc;
}
function discoverTestFiles() {
return walkFiles(testsDir)
.map(fullPath => path.relative(repoRoot, fullPath))
.filter(matchesTestGlob)
.map(repoRelativePath => path.relative(testsDir, path.join(repoRoot, repoRelativePath)))
.sort();
}
function escapeAnnotation(value, property = false) {
const escaped = value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
return property ? escaped.replace(/:/g, '%3A').replace(/,/g, '%2C') : escaped;
}
function annotateFailure(displayPath, reason, output) {
if (process.env.GITHUB_ACTIONS !== 'true') return;
const context = output.split(/\r?\n/)
.filter(line => /^\s*(?:FAIL\b|not ok\b|[A-Za-z]*Error\b|[\u2717\u274c])/i.test(line))
.slice(0, 3)
.join('\n');
const message = [reason, context].filter(Boolean).join(': ').slice(0, 1000);
console.log(`::error file=${escapeAnnotation(`tests/${displayPath}`, true)}::${escapeAnnotation(message)}`);
}
function parseCounts(combined) {
// Nested runner diagnostics may contain earlier summaries. The file's last
// standalone summary owns its totals; mentions inside diagnostics do not.
const summaries = [...combined.matchAll(/^[ \t]*(?:(?:[^\r\n:]+:[ \t]*)?Results:[ \t]*)?(Passed|Failed):[ \t]*(\d+)(?:[ \t]*,?[ \t]+(Passed|Failed):[ \t]*(\d+))?(?:[ \t]*,[ \t]*Skipped:[ \t]*\d+)?[ \t]*\r?$/gm)];
if (summaries.length) {
let passed = 0;
let failed = 0;
for (const summary of summaries) {
if (summary[1] === 'Passed') {
passed = Number(summary[2]);
failed = 0;
} else failed = Number(summary[2]);
if (summary[3] === 'Passed') passed = Number(summary[4]);
else if (summary[3] === 'Failed') failed = Number(summary[4]);
}
return { passed, failed, definite: true };
}
const lines = combined.split(/\r?\n/);
let tapPassed = 0;
let tapFailed = 0;
let tapSeen = false;
for (const line of lines) {
if (/^ok\b/.test(line.trim())) {
tapSeen = true;
tapPassed += 1;
} else if (/^not ok\b/.test(line.trim())) {
tapSeen = true;
tapFailed += 1;
}
}
if (tapSeen) {
return { passed: tapPassed, failed: tapFailed, definite: true };
}
return { passed: 0, failed: 0, definite: false };
}
const testFiles = discoverTestFiles();
const BOX_W = 58;
const boxLine = s => `║${s.padEnd(BOX_W)}║`;
console.log('╔' + '═'.repeat(BOX_W) + '╗');
console.log(boxLine(' ECC - Test Suite'));
console.log('╚' + '═'.repeat(BOX_W) + '╝');
console.log();
if (skipPatterns.length > 0) {
console.log(`Skipped patterns: ${skipPatterns.join(', ')}`);
console.log();
}
if (testFiles.length === 0) {
console.log(`✗ No test files matched ${TEST_GLOB}${filter ? ` with filter "${filter}"` : ''}`);
process.exit(1);
}
let totalPassed = 0;
let totalFailed = 0;
let filesPassed = 0;
let filesFailed = 0;
const failedFiles = [];
for (const testFile of testFiles) {
const testPath = path.join(testsDir, testFile);
const displayPath = testFile.split(path.sep).join('/');
if (!fs.existsSync(testPath)) {
console.log(`WARNING Skipping ${displayPath} (file not found)`);
continue;
}
console.log(`\n━━━ Running ${displayPath} ━━━`);
const childEnv = { ...process.env };
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR', 'GIT_PREFIX']) {
delete childEnv[key];
}
const result = spawnSync('node', [testPath], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
env: childEnv
});
const stdout = result.stdout || '';
const stderr = result.stderr || '';
if (stdout) console.log(stdout);
if (stderr) console.log(stderr);
const combined = `${stdout}\n${stderr}`;
// Counts belong to stdout when a suite prints its summary there. Stderr is
// diagnostic output and may quote the failing nested process verbatim.
const stdoutCounts = parseCounts(stdout);
const counts = stdoutCounts.definite ? stdoutCounts : parseCounts(stderr);
const processFailed = Boolean(result.error) || result.status !== 0;
let failureReason;
if (result.error) {
failureReason = `failed to start: ${result.error.message}`;
} else if (result.status !== 0) {
failureReason = result.signal
? `terminated by signal ${result.signal}`
: `exited with status ${result.status}`;
} else if (counts.definite && counts.failed > 0) {
failureReason = `reported ${counts.failed} failed tests`;
}
if (failureReason) {
filesFailed += 1;
failedFiles.push(displayPath);
totalPassed += counts.passed;
totalFailed += processFailed ? Math.max(counts.failed, 1) : counts.failed;
console.log(`✗ ${displayPath} ${failureReason}`);
annotateFailure(displayPath, failureReason, combined);
} else {
filesPassed += 1;
if (counts.definite) {
totalPassed += counts.passed;
totalFailed += counts.failed;
}
}
}
const totalTests = totalPassed + totalFailed;
console.log('\n╔' + '═'.repeat(BOX_W) + '╗');
console.log(boxLine(' Final Results'));
console.log('╠' + '═'.repeat(BOX_W) + '╣');
console.log(boxLine(` Total Tests: ${String(totalTests).padStart(4)}`));
console.log(boxLine(` Passed: ${String(totalPassed).padStart(4)} ✓`));
console.log(boxLine(` Failed: ${String(totalFailed).padStart(4)} ${totalFailed > 0 ? '✗' : ' '}`));
console.log(boxLine(` Files: ${filesPassed} passed, ${filesFailed} failed`));
if (failedFiles.length > 0) {
console.log('╠' + '═'.repeat(BOX_W) + '╣');
for (const name of failedFiles.slice(0, 10)) {
console.log(boxLine(` ✗ ${name}`.slice(0, BOX_W)));
}
if (failedFiles.length > 10) {
console.log(boxLine(` ... and ${failedFiles.length - 10} more`));
}
}
console.log('╚' + '═'.repeat(BOX_W) + '╝');
console.log(`\nPassed: ${totalPassed}, Failed: ${totalFailed}`);
process.exitCode = totalFailed > 0 ? 1 : 0;