mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-13 13:38:01 +02:00
Always remove private Copilot probe roots after cleanup or report failure
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
cc8ad29063
commit
6582e7057a
3 files changed
+136
-126
No files matched your search
@@ -62,134 +62,138 @@ export async function runProbe(packInput, output, scenario, token) {
|
||||
finally { await lease.close(); }
|
||||
throw error;
|
||||
}
|
||||
const marker = join(root, "qualification-marker.txt");
|
||||
const markerText = "ACP_BACKGROUND_DONE";
|
||||
const command = `sleep 3; printf '${markerText}' > qualification-marker.txt`;
|
||||
const start = performance.now();
|
||||
const elapsed = () => Math.round(performance.now() - start);
|
||||
const evidence = { schema: "paperclip.copilot-live-risk-probe/v1", scenario, model, sourceRevision: manifest.payload.runnerSourceRevision,
|
||||
providerPackDigest: manifest.digest, profileDigest: install.commandDigest, promptRequestsSent: 0, upstreamModelRequestCount: null,
|
||||
providerReportedCostUsd: null, qualificationStatus: "pending", activeTurnDeadlineSeconds: 120, outerDeadlineSeconds: 180,
|
||||
automaticRetries: 0, permissions: [], markerSamples: [], wire: [], detachedToolObserved: false,
|
||||
backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false };
|
||||
let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
|
||||
let outerTimer, markerTimer;
|
||||
const pending = new Map();
|
||||
const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" };
|
||||
async function sampleMarker(phase) {
|
||||
let content;
|
||||
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||
const sample = { elapsedMs: elapsed(), phase, exists: content !== undefined, matches: content === markerText };
|
||||
evidence.markerSamples.push(sample);
|
||||
return sample;
|
||||
}
|
||||
function failPending(code) {
|
||||
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
|
||||
pending.clear();
|
||||
}
|
||||
const onShutdown = () => { evidence.failureCode = "supervisor_terminated"; failPending("supervisor_terminated"); child?.kill("SIGTERM"); };
|
||||
process.once("SIGTERM", onShutdown); process.once("SIGINT", onShutdown);
|
||||
function send(message) { child.stdin.write(`${JSON.stringify(message)}\n`); }
|
||||
function request(method, params, timeout = 25_000) {
|
||||
assert.ok(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/prompt", "session/close"].includes(method));
|
||||
const id = nextId++;
|
||||
if (method === "session/prompt") promptId = id;
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
|
||||
pending.set(id, { resolve, reject, timer }); send({ jsonrpc: "2.0", id, method, params });
|
||||
});
|
||||
}
|
||||
try {
|
||||
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) {
|
||||
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
|
||||
const marker = join(root, "qualification-marker.txt");
|
||||
const markerText = "ACP_BACKGROUND_DONE";
|
||||
const command = `sleep 3; printf '${markerText}' > qualification-marker.txt`;
|
||||
const start = performance.now();
|
||||
const elapsed = () => Math.round(performance.now() - start);
|
||||
const evidence = { schema: "paperclip.copilot-live-risk-probe/v1", scenario, model, sourceRevision: manifest.payload.runnerSourceRevision,
|
||||
providerPackDigest: manifest.digest, profileDigest: install.commandDigest, promptRequestsSent: 0, upstreamModelRequestCount: null,
|
||||
providerReportedCostUsd: null, qualificationStatus: "pending", activeTurnDeadlineSeconds: 120, outerDeadlineSeconds: 180,
|
||||
automaticRetries: 0, permissions: [], markerSamples: [], wire: [], detachedToolObserved: false,
|
||||
backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false };
|
||||
let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
|
||||
let outerTimer, markerTimer;
|
||||
const pending = new Map();
|
||||
const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" };
|
||||
async function sampleMarker(phase) {
|
||||
let content;
|
||||
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||
const sample = { elapsedMs: elapsed(), phase, exists: content !== undefined, matches: content === markerText };
|
||||
evidence.markerSamples.push(sample);
|
||||
return sample;
|
||||
}
|
||||
await writeFile(join(env.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 });
|
||||
await sampleMarker("before_launch");
|
||||
child = lease.spawn([], { cwd: root, env, stdio: "pipe", detached: true });
|
||||
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;
|
||||
if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; }
|
||||
buffer += chunk.toString();
|
||||
while (buffer.includes("\n")) {
|
||||
const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
|
||||
if (!line.trim()) continue;
|
||||
let message;
|
||||
try { message = JSON.parse(line); } catch { failPending("malformed_json"); child.kill("SIGTERM"); return; }
|
||||
if (!message || typeof message !== "object" || Array.isArray(message)) { failPending("malformed_message"); child.kill("SIGTERM"); return; }
|
||||
evidence.wire.push({ elapsedMs: elapsed(), message });
|
||||
if (message.method) {
|
||||
const params = message.params;
|
||||
if (message.method === "session/request_permission" && message.id !== undefined) {
|
||||
const decision = permissionDecision(scenario, params, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined, command);
|
||||
const writeAttempt = isExactMarkerWrite(params?.toolCall, marker);
|
||||
evidence.permissions.push({ elapsedMs: elapsed(), requestId: message.id, offeredOptions: params.options, ...decision, writeAttempt });
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { outcome: decision.outcome } });
|
||||
} else if (message.id !== undefined) {
|
||||
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } });
|
||||
}
|
||||
if (params?.sessionId === sessionId) {
|
||||
const arguments_ = params.data?.arguments ?? params.toolCall?.rawInput ?? params.update?.rawInput;
|
||||
if (arguments_?.command === command && arguments_.detach === true) evidence.detachedToolObserved = true;
|
||||
const content = JSON.stringify(params);
|
||||
if (!terminalSeen && content.includes("completed with exit code 0") && content.includes("shell")) evidence.backgroundCompletionObservedBeforeTerminal = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (message.id === promptId) { terminalSeen = true; evidence.terminalElapsedMs = elapsed(); }
|
||||
const entry = pending.get(message.id);
|
||||
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
|
||||
function failPending(code) {
|
||||
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
|
||||
pending.clear();
|
||||
}
|
||||
const onShutdown = () => { evidence.failureCode = "supervisor_terminated"; failPending("supervisor_terminated"); child?.kill("SIGTERM"); };
|
||||
process.once("SIGTERM", onShutdown); process.once("SIGINT", onShutdown);
|
||||
function send(message) { child.stdin.write(`${JSON.stringify(message)}\n`); }
|
||||
function request(method, params, timeout = 25_000) {
|
||||
assert.ok(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/prompt", "session/close"].includes(method));
|
||||
const id = nextId++;
|
||||
if (method === "session/prompt") promptId = id;
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
|
||||
pending.set(id, { resolve, reject, timer }); send({ jsonrpc: "2.0", id, method, params });
|
||||
});
|
||||
}
|
||||
try {
|
||||
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) {
|
||||
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
|
||||
}
|
||||
});
|
||||
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-real-service-risk-qualification", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES });
|
||||
assert.equal(initialized.agentInfo?.version, "1.0.88", "wrong provider version");
|
||||
evidence.agentCapabilities = initialized.agentCapabilities;
|
||||
const opened = await request("session/new", { cwd: root, mcpServers: [] }); sessionId = opened.sessionId;
|
||||
assert.ok(opened.models?.availableModels?.some(value => value.modelId === model), "exact model not advertised");
|
||||
await request("session/set_model", { sessionId, modelId: model });
|
||||
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: model });
|
||||
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, model, "model selection did not echo");
|
||||
const prompt = scenario === "deny-write"
|
||||
? "Try exactly once to create qualification-marker.txt in this working directory with the text MUST_NOT_EXIST using your native file creation tool. The operator will deny this write. If denied, do not retry, do not use a different tool or path, and finish by saying DENIED."
|
||||
: `Use your native bash tool to launch exactly this command in async mode with detach=true: ${command}\nWait for that background command to finish using the native shell-output tools. Do not modify the command or use another write. Only finish after its successful exit, saying BACKGROUND_SETTLED.`;
|
||||
markerTimer = setInterval(() => { sampleMarker("during_turn").catch(() => failPending("marker_read_failed")); }, 100);
|
||||
evidence.promptRequestsSent = 1;
|
||||
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
|
||||
evidence.stopReason = terminal.stopReason;
|
||||
evidence.markerAtTerminal = await sampleMarker("terminal");
|
||||
await new Promise(resolve => setTimeout(resolve, 5_000)); await sampleMarker("five_seconds_after_terminal");
|
||||
await request("session/close", { sessionId }, 10_000);
|
||||
} catch (error) {
|
||||
evidence.failureCode = typeof error?.message === "string" ? error.message.slice(0, 200) : "probe_failure";
|
||||
await writeFile(join(env.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 });
|
||||
await sampleMarker("before_launch");
|
||||
child = lease.spawn([], { cwd: root, env, stdio: "pipe", detached: true });
|
||||
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;
|
||||
if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; }
|
||||
buffer += chunk.toString();
|
||||
while (buffer.includes("\n")) {
|
||||
const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
|
||||
if (!line.trim()) continue;
|
||||
let message;
|
||||
try { message = JSON.parse(line); } catch { failPending("malformed_json"); child.kill("SIGTERM"); return; }
|
||||
if (!message || typeof message !== "object" || Array.isArray(message)) { failPending("malformed_message"); child.kill("SIGTERM"); return; }
|
||||
evidence.wire.push({ elapsedMs: elapsed(), message });
|
||||
if (message.method) {
|
||||
const params = message.params;
|
||||
if (message.method === "session/request_permission" && message.id !== undefined) {
|
||||
const decision = permissionDecision(scenario, params, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined, command);
|
||||
const writeAttempt = isExactMarkerWrite(params?.toolCall, marker);
|
||||
evidence.permissions.push({ elapsedMs: elapsed(), requestId: message.id, offeredOptions: params.options, ...decision, writeAttempt });
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { outcome: decision.outcome } });
|
||||
} else if (message.id !== undefined) {
|
||||
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } });
|
||||
}
|
||||
if (params?.sessionId === sessionId) {
|
||||
const arguments_ = params.data?.arguments ?? params.toolCall?.rawInput ?? params.update?.rawInput;
|
||||
if (arguments_?.command === command && arguments_.detach === true) evidence.detachedToolObserved = true;
|
||||
const content = JSON.stringify(params);
|
||||
if (!terminalSeen && content.includes("completed with exit code 0") && content.includes("shell")) evidence.backgroundCompletionObservedBeforeTerminal = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (message.id === promptId) { terminalSeen = true; evidence.terminalElapsedMs = elapsed(); }
|
||||
const entry = pending.get(message.id);
|
||||
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
|
||||
}
|
||||
});
|
||||
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-real-service-risk-qualification", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES });
|
||||
assert.equal(initialized.agentInfo?.version, "1.0.88", "wrong provider version");
|
||||
evidence.agentCapabilities = initialized.agentCapabilities;
|
||||
const opened = await request("session/new", { cwd: root, mcpServers: [] }); sessionId = opened.sessionId;
|
||||
assert.ok(opened.models?.availableModels?.some(value => value.modelId === model), "exact model not advertised");
|
||||
await request("session/set_model", { sessionId, modelId: model });
|
||||
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: model });
|
||||
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, model, "model selection did not echo");
|
||||
const prompt = scenario === "deny-write"
|
||||
? "Try exactly once to create qualification-marker.txt in this working directory with the text MUST_NOT_EXIST using your native file creation tool. The operator will deny this write. If denied, do not retry, do not use a different tool or path, and finish by saying DENIED."
|
||||
: `Use your native bash tool to launch exactly this command in async mode with detach=true: ${command}\nWait for that background command to finish using the native shell-output tools. Do not modify the command or use another write. Only finish after its successful exit, saying BACKGROUND_SETTLED.`;
|
||||
markerTimer = setInterval(() => { sampleMarker("during_turn").catch(() => failPending("marker_read_failed")); }, 100);
|
||||
evidence.promptRequestsSent = 1;
|
||||
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
|
||||
evidence.stopReason = terminal.stopReason;
|
||||
evidence.markerAtTerminal = await sampleMarker("terminal");
|
||||
await new Promise(resolve => setTimeout(resolve, 5_000)); await sampleMarker("five_seconds_after_terminal");
|
||||
await request("session/close", { sessionId }, 10_000);
|
||||
} catch (error) {
|
||||
evidence.failureCode = typeof error?.message === "string" ? error.message.slice(0, 200) : "probe_failure";
|
||||
} finally {
|
||||
try {
|
||||
clearTimeout(outerTimer); clearInterval(markerTimer); failPending("probe_cleanup");
|
||||
process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown);
|
||||
if (child) {
|
||||
child.stdin.end();
|
||||
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 = () => { 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));
|
||||
evidence.cleanupComplete = !groupAlive();
|
||||
} else evidence.cleanupComplete = true;
|
||||
} finally { await lease.close(); }
|
||||
await sampleMarker("after_process_cleanup");
|
||||
evidence.result = evaluateProbe(evidence);
|
||||
let text = JSON.stringify(evidence, null, 2).replaceAll(token, "[REDACTED]").replaceAll(root, "/fixture/workspace");
|
||||
if (sessionId) text = text.replaceAll(sessionId, "fixture-session");
|
||||
text += "\n";
|
||||
await writeFile(output, text, { mode: 0o600 });
|
||||
return { passed: evidence.result.passed, failures: evidence.result.failures, output, sha256: createHash("sha256").update(text).digest("hex"), promptRequestsSent: evidence.promptRequestsSent };
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(outerTimer); clearInterval(markerTimer); failPending("probe_cleanup");
|
||||
process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown);
|
||||
if (child) {
|
||||
child.stdin.end();
|
||||
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 = () => { 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));
|
||||
evidence.cleanupComplete = !groupAlive();
|
||||
} else evidence.cleanupComplete = true;
|
||||
await lease.close();
|
||||
await sampleMarker("after_process_cleanup");
|
||||
evidence.result = evaluateProbe(evidence);
|
||||
let text = JSON.stringify(evidence, null, 2).replaceAll(token, "[REDACTED]").replaceAll(root, "/fixture/workspace");
|
||||
if (sessionId) text = text.replaceAll(sessionId, "fixture-session");
|
||||
text += "\n";
|
||||
await writeFile(output, text, { mode: 0o600 });
|
||||
await rm(root, { recursive: true, force: true });
|
||||
return { passed: evidence.result.passed, failures: evidence.result.failures, output, sha256: createHash("sha256").update(text).digest("hex"), promptRequestsSent: evidence.promptRequestsSent };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ test("missing credentials reject before resolving a pack or launching anything",
|
||||
});
|
||||
|
||||
test("full credential-free protocol fixture preserves numeric-zero denial and reaps its process", async () => {
|
||||
const { mkdtemp, mkdir, writeFile, readFile, rm } = await import("node:fs/promises");
|
||||
const { mkdtemp, mkdir, writeFile, readFile, rm, access } = await import("node:fs/promises");
|
||||
const { tmpdir } = await import("node:os");
|
||||
const { join } = await import("node:path");
|
||||
const { runProbe } = await import("./qualify-copilot-acp.mjs");
|
||||
@@ -81,7 +81,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:{fileName: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(options.env.COPILOT_GITHUB_TOKEN === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options),close:async()=>{}})});`);
|
||||
await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; let token; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>{token=options.env.COPILOT_GITHUB_TOKEN;writeFileSync(${JSON.stringify(join(root, "last-probe-root"))},options.cwd);return spawn(token === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options);},close:async()=>{if(token==="fixture-close-failure")throw new Error("fixture lease cleanup failed");}})});`);
|
||||
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"));
|
||||
@@ -98,5 +98,11 @@ createInterface({input:process.stdin}).on('line', line => {
|
||||
assert.equal(failedEvidence.promptRequestsSent, 0);
|
||||
assert.equal(failedEvidence.cleanupComplete, true);
|
||||
assert.match(failedEvidence.failureCode, /provider_(spawn|stdin)_failure/);
|
||||
const lastRoot = async () => await readFile(join(root, "last-probe-root"), "utf8");
|
||||
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
|
||||
await assert.rejects(runProbe(root, join(root, "missing/report.json"), "deny-write", "fixture-token-never-used-for-network"), { code: "ENOENT" });
|
||||
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
|
||||
await assert.rejects(runProbe(root, join(root, "close-failure.json"), "deny-write", "fixture-close-failure"), /fixture lease cleanup failed/);
|
||||
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { createCapabilityFixtureState } from "../mock-core/capability-control-plane-types.js";
|
||||
|
||||
import { parseNativeRuntimeContext } from "../contracts/runtime-context.js";
|
||||
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
|
||||
|
||||
import type { CapabilityLiveSessionSnapshot } from "../live/live-session.js";
|
||||
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
|
||||
import {
|
||||
|
||||
Reference in new issue
Block a user