fix: Test-Timeout verhindert hängenden Loop

runTestsParallel() wrappte jede Test-Suite mit 'timeout N bash -c ...'
(Standard: 120s). Exit 124 wird als Timeout erkannt und im Output markiert.

Neues Flag --test-timeout N für Integration-Tests die länger brauchen:
  /optimize "..." --test-timeout 300

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-05-20 23:47:06 +02:00
commit a6f7f968b5

View file

@ -452,10 +452,16 @@ async function detectTestCommands(
async function runTestsParallel( async function runTestsParallel(
pi: ExtensionAPI, pi: ExtensionAPI,
ctx: ExtensionCommandContext, ctx: ExtensionCommandContext,
cmds: string[] cmds: string[],
timeoutSecs: number = 120
): Promise<string> { ): Promise<string> {
const results = await Promise.all( const results = await Promise.all(
cmds.map(cmd => pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd })) // timeout-Wrapper: verhindert hängende Tests (Exit 124 = Timeout)
cmds.map(cmd => pi.exec(
"bash",
["-c", `timeout ${timeoutSecs} bash -c ${JSON.stringify(cmd)}`],
{ cwd: ctx.cwd }
))
); );
const MAX_PER = Math.max(1000, Math.floor(6000 / cmds.length)); const MAX_PER = Math.max(1000, Math.floor(6000 / cmds.length));
return results.map((r, i) => { return results.map((r, i) => {
@ -463,7 +469,9 @@ async function runTestsParallel(
const out = raw.length > MAX_PER const out = raw.length > MAX_PER
? raw.slice(0, MAX_PER) + `\n[… gekürzt, ${raw.length} Zeichen]` ? raw.slice(0, MAX_PER) + `\n[… gekürzt, ${raw.length} Zeichen]`
: raw || "(kein Output)"; : raw || "(kein Output)";
const status = r.code === 0 ? "✓ OK" : `✗ Exit ${r.code}`; const status = r.code === 0 ? "✓ OK"
: r.code === 124 ? `✗ Timeout (>${timeoutSecs}s)`
: `✗ Exit ${r.code}`;
return `=== ${cmds[i]} [${status}] ===\n${out}`; return `=== ${cmds[i]} [${status}] ===\n${out}`;
}).join("\n\n"); }).join("\n\n");
} }
@ -706,17 +714,19 @@ export default function (pi: ExtensionAPI) {
// ── Automatische Optimierungsschleife ──────────────────────────────────── // ── Automatische Optimierungsschleife ────────────────────────────────────
pi.registerCommand("optimize", { pi.registerCommand("optimize", {
description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"]", description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"] [--test-timeout N]",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); const roundsMatch = (args || "").match(/--rounds\s+(\d+)/);
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3;
const withDoku = /--with-doku/.test(args || ""); const withDoku = /--with-doku/.test(args || "");
const continueMode = /--continue/.test(args || ""); const continueMode = /--continue/.test(args || "");
// --test-cmd "befehl" oder --test-cmd befehl (ohne Leerzeichen im Befehl)
const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+(\S+)/); const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+(\S+)/);
const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2]) : null; const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2]) : null;
const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/);
const testTimeout = testTimeoutMatch ? parseInt(testTimeoutMatch[1], 10) : 120;
const task = (args || "") const task = (args || "")
.replace(/--rounds\s+\d+/, "") .replace(/--rounds\s+\d+/, "")
.replace(/--test-timeout\s+\d+/, "")
.replace(/--with-doku/, "") .replace(/--with-doku/, "")
.replace(/--continue/, "") .replace(/--continue/, "")
.replace(/--test-cmd\s+"[^"]*"/, "") .replace(/--test-cmd\s+"[^"]*"/, "")
@ -793,8 +803,8 @@ export default function (pi: ExtensionAPI) {
const label = autoTestCmds.length === 1 const label = autoTestCmds.length === 1
? autoTestCmds[0].split(" ")[0] ? autoTestCmds[0].split(" ")[0]
: `${autoTestCmds.length} Suiten parallel`; : `${autoTestCmds.length} Suiten parallel`;
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label})…`); ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`);
const testOutput = await runTestsParallel(pi, ctx, autoTestCmds); const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout);
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`);
await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
} else { } else {