fix(tests): guard empty arrays, cap output, kill orphaned children

- bash 3.2: guard BLOCKERS/PATH_ARGS empty-array expansion under set -u
- run-coverage-lane: surface collectLaneFiles errors instead of throwing
- run-test-lane: cap per-file output at 1 MB; track and SIGKILL children on SIGINT/SIGTERM
This commit is contained in:
2026-07-06 23:58:42 -07:00
parent a4927a3bbd
commit db9121a5f9
3 changed files with 55 additions and 5 deletions
@@ -255,8 +255,18 @@ write_summary_files() {
local lane_lines local lane_lines
lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}") lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}")
printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt" printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt"
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt" # bash 3.2 raises "unbound variable" under set -u when expanding an empty
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt" # array, so guard on length (matching the idiom used elsewhere here).
if [[ ${#BLOCKERS[@]} -gt 0 ]]; then
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt"
else
: >"$ARTIFACT_DIR/blockers.txt"
fi
if [[ ${#PATH_ARGS[@]} -gt 0 ]]; then
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt"
else
: >"$ARTIFACT_DIR/requested-paths.txt"
fi
ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \ ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \
SESSION_ID_ENV="$SESSION_ID" \ SESSION_ID_ENV="$SESSION_ID" \
+7 -1
View File
@@ -214,7 +214,13 @@ function runCoverageLane(): number {
rmSync(shardRoot, { recursive: true, force: true }); rmSync(shardRoot, { recursive: true, force: true });
mkdirSync(shardRoot, { recursive: true }); mkdirSync(shardRoot, { recursive: true });
const files = collectLaneFiles(repoRoot, laneName); let files: string[];
try {
files = collectLaneFiles(repoRoot, laneName);
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
return 1;
}
const reports: string[] = []; const reports: string[] = [];
try { try {
+36 -2
View File
@@ -11,6 +11,28 @@ import { collectLaneFiles } from './test-lanes.ts';
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
// Cap per-file buffered output so a long or noisy test cannot grow the string
// without bound and exhaust memory.
const MAX_OUTPUT_BYTES = 1024 * 1024;
// Track spawned `bun test` children so we can kill them if the runner is
// interrupted, avoiding orphaned in-flight test processes.
const activeChildren = new Set();
function terminateChildren() {
for (const child of activeChildren) {
child.kill('SIGKILL');
}
activeChildren.clear();
}
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
terminateChildren();
process.exit(130);
});
}
function parseArgs(argv) { function parseArgs(argv) {
const options = { lane: undefined, jobs: 1, timeoutSecs: 300, singleProcess: false }; const options = { lane: undefined, jobs: 1, timeoutSecs: 300, singleProcess: false };
for (let index = 0; index < argv.length; index += 1) { for (let index = 0; index < argv.length; index += 1) {
@@ -34,20 +56,32 @@ function parseArgs(argv) {
function runFile(file, timeoutSecs) { function runFile(file, timeoutSecs) {
return new Promise((resolvePromise) => { return new Promise((resolvePromise) => {
const child = spawn('bun', ['test', `./${file}`], { cwd: repoRoot }); const child = spawn('bun', ['test', `./${file}`], { cwd: repoRoot });
activeChildren.add(child);
let output = ''; let output = '';
let truncated = false;
let timedOut = false; let timedOut = false;
child.stdout.on('data', (chunk) => (output += chunk)); const append = (chunk) => {
child.stderr.on('data', (chunk) => (output += chunk)); if (truncated) return;
output += chunk;
if (output.length > MAX_OUTPUT_BYTES) {
output = `${output.slice(0, MAX_OUTPUT_BYTES)}\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]\n`;
truncated = true;
}
};
child.stdout.on('data', append);
child.stderr.on('data', append);
const timer = setTimeout(() => { const timer = setTimeout(() => {
timedOut = true; timedOut = true;
child.kill('SIGKILL'); child.kill('SIGKILL');
}, timeoutSecs * 1000); }, timeoutSecs * 1000);
child.on('close', (code) => { child.on('close', (code) => {
clearTimeout(timer); clearTimeout(timer);
activeChildren.delete(child);
resolvePromise({ file, code: timedOut ? 124 : (code ?? 1), output, timedOut }); resolvePromise({ file, code: timedOut ? 124 : (code ?? 1), output, timedOut });
}); });
child.on('error', (error) => { child.on('error', (error) => {
clearTimeout(timer); clearTimeout(timer);
activeChildren.delete(child);
resolvePromise({ file, code: 1, output: String(error), timedOut: false }); resolvePromise({ file, code: 1, output: String(error), timedOut: false });
}); });
}); });