Files
awesome-copilot/skills/batch-script-progress/tests/verify-examples.mjs
T

136 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const windows = process.platform === 'win32';
const runtimes = [
{ name: 'Python', command: process.env.BATCH_PYTHON ?? (windows ? 'py.exe' : 'python3'), args: [...(windows && !process.env.BATCH_PYTHON ? ['-3'] : []), '-B', join(root, 'examples/batch.py')] },
{ name: 'PowerShell', command: process.env.BATCH_POWERSHELL ?? (windows ? 'powershell.exe' : 'pwsh'), args: ['-NoProfile', '-NonInteractive', ...(windows ? ['-ExecutionPolicy', 'Bypass'] : []), '-File', join(root, 'examples/batch.ps1')] },
{ name: 'Node.js', command: process.execPath, args: [join(root, 'examples/batch.mjs')] },
];
const cases = {
normal: { total: 3, succeeded: 3, failed: 0, skipped: 0, status: 'completed', code: 0 },
mixed: { total: 4, succeeded: 2, failed: 1, skipped: 1, status: 'partial', code: 1 },
'partial-zero': { total: 3, succeeded: 2, failed: 1, skipped: 0, status: 'partial', code: 0 },
empty: { total: 0, succeeded: 0, failed: 0, skipped: 0, status: 'completed', code: 0 },
unknown: { total: null, succeeded: 3, failed: 0, skipped: 0, status: 'completed', code: 0 },
'fail-fast': { total: 3, succeeded: 1, failed: 1, skipped: 0, status: 'failed', code: 1 },
cancelled: { total: 3, succeeded: 1, failed: 0, skipped: 0, status: 'cancelled', code: 130 },
live: { total: 3, succeeded: 3, failed: 0, skipped: 0, status: 'completed', code: 0 },
};
function accounting(record) {
for (const key of ['processed', 'succeeded', 'failed', 'skipped']) {
assert.ok(Number.isInteger(record[key]) && record[key] >= 0, key);
}
assert.equal(record.processed, record.succeeded + record.failed + record.skipped);
}
function verify(stdout, stderr, code, scenario) {
const expected = cases[scenario];
const events = stderr.trim().split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
assert.equal(events[0].event, 'start');
assert.equal(events.at(-1).event, 'summary');
assert.equal(events.filter(record => record.event === 'start').length, 1);
assert.equal(events.filter(record => record.event === 'summary').length, 1);
let previous = { processed: 0, succeeded: 0, failed: 0, skipped: 0, elapsed_ms: 0 };
for (const record of events) {
accounting(record);
assert.equal(record.total, expected.total);
for (const key of ['processed', 'succeeded', 'failed', 'skipped', 'elapsed_ms']) {
assert.ok(record[key] >= previous[key], `non-monotonic ${key}`);
}
if (expected.total === null) {
for (const key of ['percentage', 'eta', 'remaining']) assert.ok(!(key in record));
}
previous = record;
}
const summary = events.at(-1);
for (const key of ['succeeded', 'failed', 'skipped', 'status']) assert.equal(summary[key], expected[key]);
assert.equal(summary.exit_code, expected.code);
assert.equal(code, expected.code);
assert.equal(summary.unprocessed, expected.total === null ? null : expected.total - summary.processed);
const failures = events.filter(record => record.event === 'item-failed');
assert.equal(failures.length, expected.failed);
for (const failure of failures) {
assert.equal(failure.item_id, ['fail-fast', 'partial-zero'].includes(scenario) ? 'item-2' : 'item-3');
assert.equal(failure.reason, 'fixture-rejected');
assert.equal(failure.will_continue, scenario !== 'fail-fast');
}
if (scenario === 'empty') assert.equal(events.filter(record => record.event === 'progress').length, 0);
if (['normal', 'unknown', 'mixed', 'partial-zero', 'live'].includes(scenario)) assert.ok(events.some(record => record.event === 'progress'));
if (scenario === 'cancelled') {
assert.equal(stdout, '');
} else {
const result = JSON.parse(stdout);
const ids = scenario === 'mixed' ? [1, 4] : scenario === 'partial-zero' ? [1, 3] : scenario === 'fail-fast' ? [1] : scenario === 'empty' ? [] : [1, 2, 3];
assert.deepEqual(result.results, ids.map(index => ({ id: `item-${index}`, value: index * 2 })));
}
}
async function verifyLive(runtime) {
const child = spawn(runtime.command, [...runtime.args, 'live'], { stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
let pending = '';
let released = false;
let failure;
const timeout = setTimeout(() => {
failure = new Error(`${runtime.name}: no live notification before watchdog`);
child.kill();
}, 15000);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdin.on('error', error => { failure ??= error; });
child.stdout.on('data', chunk => { stdout += chunk; });
child.stderr.on('data', chunk => {
stderr += chunk;
pending += chunk;
let newline;
while ((newline = pending.indexOf('\n')) !== -1) {
const line = pending.slice(0, newline).trim();
pending = pending.slice(newline + 1);
try {
const record = JSON.parse(line);
if (record.event === 'progress' && !released) {
assert.equal(child.exitCode, null);
assert.equal(child.signalCode, null);
assert.equal(record.processed, 1);
released = true;
child.stdin.end('\n');
}
} catch (error) {
failure ??= error;
child.kill();
}
}
});
const code = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', resolve);
}).finally(() => clearTimeout(timeout));
if (failure) throw failure;
assert.ok(released, `${runtime.name}: notification not observed before exit`);
verify(stdout, stderr, code, 'live');
}
assert.throws(() => accounting({ processed: 2, succeeded: 1, failed: 0, skipped: 0 }), assert.AssertionError);
console.log('PASS negative control: corrupted accounting rejected');
let passed = 0;
for (const runtime of runtimes) {
for (const scenario of Object.keys(cases).filter(name => name !== 'live')) {
const result = spawnSync(runtime.command, [...runtime.args, scenario], { encoding: 'utf8', timeout: 15000 });
if (result.error) throw result.error;
assert.equal(result.signal, null);
verify(result.stdout, result.stderr, result.status, scenario);
console.log(`PASS ${runtime.name} ${scenario}`);
passed++;
}
await verifyLive(runtime);
console.log(`PASS ${runtime.name} live output received before release and exit`);
passed++;
}
console.log(`BATCH_OUTPUT_CONTRACT_PASSED scenarios=${passed}`);