diff --git a/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs b/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs index 9d4e41bf86..038ad0b0e5 100644 --- a/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs +++ b/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs @@ -95,6 +95,7 @@ export async function runProbe(packInput, output, scenario, token) { exitPromise = new Promise(resolve => child.once("close", (code, signal) => { failPending("provider_exited"); resolve({ code, signal }); })); outerTimer = setTimeout(() => { failPending("outer_deadline"); child.kill("SIGTERM"); }, 180_000); child.once("error", () => failPending("provider_spawn_failure")); + child.stdin.on("error", () => failPending("provider_stdin_failure")); child.stderr.on("data", () => {}); child.stdout.on("data", chunk => { bytes += chunk.length; @@ -155,11 +156,12 @@ export async function runProbe(packInput, output, scenario, token) { process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown); if (child) { child.stdin.end(); - const killGroup = signal => { try { process.kill(-child.pid, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } }; + const groupId = Number.isSafeInteger(child.pid) && child.pid > 0 ? child.pid : null; + const killGroup = signal => { if (groupId === null) return; try { process.kill(-groupId, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } }; const term = setTimeout(() => killGroup("SIGTERM"), 1_000), kill = setTimeout(() => killGroup("SIGKILL"), 5_000); evidence.providerExit = await exitPromise; clearTimeout(term); clearTimeout(kill); killGroup("SIGTERM"); - const groupAlive = () => { try { process.kill(-child.pid, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; } }; + const groupAlive = () => { if (groupId === null) return false; try { process.kill(-groupId, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; } }; for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50)); if (groupAlive()) killGroup("SIGKILL"); for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50)); diff --git a/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs b/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs index 4ff23fa8c6..44edbb1597 100644 --- a/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs +++ b/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs @@ -55,7 +55,7 @@ createInterface({input:process.stdin}).on('line', line => { if(m.method==='session/prompt'){promptId=m.id;send({id:0,method:'session/request_permission',params:{sessionId:'fixture-session',toolCall:{kind:'edit',rawInput:{path:process.cwd()+'/qualification-marker.txt'}},options:${JSON.stringify(options)}}});return;} send({id:m.id,result}); }).on('close',()=>process.exit(0));`); - await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>spawn(${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options),close:async()=>{}})});`); + await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>spawn(options.env.COPILOT_GITHUB_TOKEN === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options),close:async()=>{}})});`); const output = join(root, "output.json"); const result = await runProbe(root, output, "deny-write", "fixture-token-never-used-for-network"); const evidence = JSON.parse(await readFile(output, "utf8")); @@ -65,5 +65,12 @@ createInterface({input:process.stdin}).on('line', line => { assert.equal(evidence.cleanupComplete, true); assert.ok(evidence.markerSamples.every(sample => !sample.exists)); assert.equal(evidence.providerReportedCostUsd, null); + const failedOutput = join(root, "spawn-failure.json"); + const failed = await runProbe(root, failedOutput, "deny-write", "fixture-spawn-failure"); + const failedEvidence = JSON.parse(await readFile(failedOutput, "utf8")); + assert.equal(failed.passed, false); + assert.equal(failedEvidence.promptRequestsSent, 0); + assert.equal(failedEvidence.cleanupComplete, true); + assert.match(failedEvidence.failureCode, /provider_(spawn|stdin)_failure/); } finally { await rm(root, { recursive: true, force: true }); } });