fix: harden command timeout defaults

This commit is contained in:
Shakker 2026-05-01 15:25:32 +01:00
parent 96b4409f03
commit fb5b84b410
No known key found for this signature in database
2 changed files with 44 additions and 1 deletions

View File

@ -2,6 +2,8 @@ import { spawn, spawnSync } from "node:child_process";
import { startResourceSampler } from "./collectors/resources.mjs";
import { repoRoot } from "./paths.mjs";
const defaultCommandTimeoutMs = 120000;
export function checkCommand(command, args) {
const result = spawnSync(command, args, {
encoding: "utf8",
@ -17,6 +19,7 @@ export function checkCommand(command, args) {
}
export function runCommand(command, options = {}) {
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
const startedAtEpochMs = Date.now();
const startedAt = new Date(startedAtEpochMs).toISOString();
return new Promise((resolve) => {
@ -41,7 +44,7 @@ export function runCommand(command, options = {}) {
timedOut = true;
child.kill("SIGTERM");
setTimeout(() => child.kill("SIGKILL"), 3000).unref();
}, options.timeoutMs);
}, timeoutMs);
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
@ -101,6 +104,14 @@ export function quoteShell(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`;
}
function normalizeTimeoutMs(value) {
const timeoutMs = value === undefined ? defaultCommandTimeoutMs : Number(value);
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
throw new Error(`runCommand timeoutMs must be a positive integer, got ${JSON.stringify(value)}`);
}
return timeoutMs;
}
function truncate(value, limit = 20000) {
if (value.length <= limit) {
return value;

View File

@ -103,6 +103,7 @@ export async function runSelfCheck(flags = {}) {
"external-cli codex is not usable"
));
checks.push(await externalCliRunAuthVerificationCheck(tmp));
checks.push(await commandTimeoutContractCheck());
checks.push(ocmCommandBuildersCheck());
checks.push(evaluationViolationHelpersCheck());
checks.push(await jsonCommandCheck("plan-json", "node bin/kova.mjs plan --json", (data) => {
@ -4616,6 +4617,37 @@ async function externalCliRunAuthVerificationCheck(tmp) {
};
}
async function commandTimeoutContractCheck() {
const command = "node -e 'setTimeout(() => console.log(\"default-timeout-ok\"), 20)'";
try {
const result = await runCommand(command, { maxOutputChars: 100000 });
assertEqual(result.status, 0, "default timeout command status");
assertEqual(result.timedOut, false, "default timeout should not expire immediately");
assertEqual(result.stdout.trim(), "default-timeout-ok", "default timeout command output");
let invalidRejected = false;
try {
await runCommand("node -e 'process.exit(0)'", { timeoutMs: 0 });
} catch (error) {
invalidRejected = /timeoutMs must be a positive integer/.test(error.message);
}
assertEqual(invalidRejected, true, "invalid timeout rejected");
return {
id: "command-timeout-contract",
status: "PASS",
command: "evaluate runCommand timeout defaults",
durationMs: result.durationMs
};
} catch (error) {
return {
id: "command-timeout-contract",
status: "FAIL",
command,
durationMs: 0,
message: error.message
};
}
}
async function failingCommandCheck(id, command, expectedMessage) {
const result = await runCommand(command, { timeoutMs: 30000, maxOutputChars: 1000000 });
const output = `${result.stdout}\n${result.stderr}`;