diff --git a/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh b/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh index a03ba636..9a16d906 100755 --- a/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh +++ b/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh @@ -255,8 +255,18 @@ write_summary_files() { local lane_lines lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}") printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt" - printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt" - printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt" + # bash 3.2 raises "unbound variable" under set -u when expanding an empty + # 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" \ SESSION_ID_ENV="$SESSION_ID" \ diff --git a/scripts/run-coverage-lane.ts b/scripts/run-coverage-lane.ts index c1248e8e..3140a623 100644 --- a/scripts/run-coverage-lane.ts +++ b/scripts/run-coverage-lane.ts @@ -214,7 +214,13 @@ function runCoverageLane(): number { rmSync(shardRoot, { recursive: true, force: 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[] = []; try { diff --git a/scripts/run-test-lane.mjs b/scripts/run-test-lane.mjs index 1bc25ace..82106baa 100644 --- a/scripts/run-test-lane.mjs +++ b/scripts/run-test-lane.mjs @@ -11,6 +11,28 @@ import { collectLaneFiles } from './test-lanes.ts'; 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) { const options = { lane: undefined, jobs: 1, timeoutSecs: 300, singleProcess: false }; for (let index = 0; index < argv.length; index += 1) { @@ -34,20 +56,32 @@ function parseArgs(argv) { function runFile(file, timeoutSecs) { return new Promise((resolvePromise) => { const child = spawn('bun', ['test', `./${file}`], { cwd: repoRoot }); + activeChildren.add(child); let output = ''; + let truncated = false; let timedOut = false; - child.stdout.on('data', (chunk) => (output += chunk)); - child.stderr.on('data', (chunk) => (output += chunk)); + const append = (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(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutSecs * 1000); child.on('close', (code) => { clearTimeout(timer); + activeChildren.delete(child); resolvePromise({ file, code: timedOut ? 124 : (code ?? 1), output, timedOut }); }); child.on('error', (error) => { clearTimeout(timer); + activeChildren.delete(child); resolvePromise({ file, code: 1, output: String(error), timedOut: false }); }); });