import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest" ;
import { killProcessTree } from "../process/kill-tree.js" ;
const supervisorMockState = vi.hoisted(() => ({
cancelReasons: [] as Array<"manual-cancel" | "overall-timeout" >,
spawnInputs: [] as Array<{ timeoutMs?: number }>,
}));
vi.mock("../process/supervisor/index.js" , () => {
let counter = 0 ;
return {
getProcessSupervisor: () => ({
spawn: async (input: { timeoutMs?: number }) => {
supervisorMockState.spawnInputs.push(input);
const runId = `mock-run-${++counter}`;
let settled = false ;
let settle = (_reason: "manual-cancel" | "overall-timeout" , _timedOut: boolean ) => {};
const waitPromise = new Promise<{
reason: "manual-cancel" | "overall-timeout" ;
exitCode: number | null ;
exitSignal: NodeJS.Signals | number | null ;
durationMs: number;
stdout: string;
stderr: string;
timedOut: boolean ;
noOutputTimedOut: boolean ;
}>((resolve) => {
settle = (reason, timedOut) => {
if (settled) {
return ;
}
settled = true ;
resolve({
reason,
exitCode: null ,
exitSignal: null ,
durationMs: input.timeoutMs ?? 0 ,
stdout: "" ,
stderr: "" ,
timedOut,
noOutputTimedOut: false ,
});
};
if (input.timeoutMs !== undefined) {
setTimeout(() => settle("overall-timeout" , true ), 12 );
}
});
return {
runId,
startedAtMs: Date.now(),
stdin: undefined,
wait: () => waitPromise,
cancel: () => {
supervisorMockState.cancelReasons.push("manual-cancel" );
settle("manual-cancel" , false );
},
};
},
cancel: vi.fn(),
cancelScope: vi.fn(),
reconcileOrphans: vi.fn(),
getRecord: vi.fn(),
}),
};
});
vi.mock("../infra/shell-env.js" , () => ({
getShellPathFromLoginShell: vi.fn(() => null ),
resolveShellEnvFallbackTimeoutMs: vi.fn(() => 0 ),
}));
vi.mock("./bash-tools.exec-host-gateway.js" , () => ({
processGatewayAllowlist: vi.fn(async () => ({})),
}));
vi.mock("./bash-tools.exec-host-node.js" , () => ({
executeNodeHostCommand: vi.fn(async () => {
throw new Error("node host not expected in background abort tests" );
}),
}));
const BACKGROUND_HOLD_CMD =
process.platform === "win32" ? 'node -e "setTimeout(() => {}, 1000)"' : "exec sleep 1" ;
const ABORT_SETTLE_MS = process.platform === "win32" ? 200 : 0 ;
const POLL_INTERVAL_MS = process.platform === "win32" ? 15 : 5 ;
const FINISHED_WAIT_TIMEOUT_MS = process.platform === "win32" ? 8 _000 : 1 _000 ;
const BACKGROUND_TIMEOUT_SEC = process.platform === "win32" ? 0 .2 : 0 .02 ;
const TEST_EXEC_DEFAULTS = {
host: "gateway" as const ,
security: "full" as const ,
ask: "off" as const ,
};
let createExecTool: typeof import ("./bash-tools.exec.js" ).createExecTool;
let getFinishedSession: typeof import ("./bash-process-registry.js" ).getFinishedSession;
let getSession: typeof import ("./bash-process-registry.js" ).getSession;
let resetProcessRegistryForTests: typeof import ("./bash-process-registry.js" ).resetProcessRegistryForTests;
type ExecToolExecuteParams = Parameters<ReturnType<typeof createExecTool>["execute" ]>[1 ];
const createTestExecTool = (
defaults?: Parameters<typeof createExecTool>[0 ],
): ReturnType<typeof createExecTool> => createExecTool({ ...TEST_EXEC_DEFAULTS, ...defaults });
beforeAll(async () => {
({ createExecTool } = await import ("./bash-tools.exec.js" ));
({ getFinishedSession, getSession, resetProcessRegistryForTests } =
await import ("./bash-process-registry.js" ));
});
beforeEach(() => {
vi.clearAllMocks();
supervisorMockState.cancelReasons.length = 0 ;
supervisorMockState.spawnInputs.length = 0 ;
});
afterEach(() => {
resetProcessRegistryForTests();
});
async function waitForFinishedSession(sessionId: string) {
let finished = getFinishedSession(sessionId);
await expect
.poll(
() => {
finished = getFinishedSession(sessionId);
return Boolean (finished);
},
{
timeout: FINISHED_WAIT_TIMEOUT_MS,
interval: POLL_INTERVAL_MS,
},
)
.toBe(true );
return finished;
}
function cleanupRunningSession(sessionId: string) {
const running = getSession(sessionId);
const pid = running?.pid;
if (pid) {
killProcessTree(pid);
}
return running;
}
async function expectBackgroundSessionSurvivesAbort(params: {
tool: ReturnType<typeof createExecTool>;
executeParams: ExecToolExecuteParams;
}) {
const abortController = new AbortController();
const result = await params.tool.execute(
"toolcall" ,
params.executeParams,
abortController.signal,
);
expect(result.details.status).toBe("running" );
const sessionId = (result.details as { sessionId: string }).sessionId;
abortController.abort();
if (ABORT_SETTLE_MS > 0 ) {
await new Promise((resolve) => setTimeout(resolve, ABORT_SETTLE_MS));
}
const running = getSession(sessionId);
const finished = getFinishedSession(sessionId);
try {
expect(supervisorMockState.cancelReasons).toEqual([]);
expect(finished).toBeUndefined();
expect(running?.exited).toBe(false );
} finally {
cleanupRunningSession(sessionId);
}
}
async function expectBackgroundSessionTimesOut(params: {
tool: ReturnType<typeof createExecTool>;
executeParams: ExecToolExecuteParams;
signal?: AbortSignal;
abortAfterStart?: boolean ;
}) {
const abortController = new AbortController();
const signal = params.signal ?? abortController.signal;
const result = await params.tool.execute("toolcall" , params.executeParams, signal);
expect(result.details.status).toBe("running" );
const sessionId = (result.details as { sessionId: string }).sessionId;
if (params.abortAfterStart) {
abortController.abort();
}
const finished = await waitForFinishedSession(sessionId);
try {
expect(finished).toBeTruthy();
expect(finished?.status).toBe("failed" );
} finally {
cleanupRunningSession(sessionId);
}
}
test("background exec is not killed when tool signal aborts" , async () => {
const tool = createTestExecTool({ allowBackground: true , backgroundMs: 0 });
await expectBackgroundSessionSurvivesAbort({
tool,
executeParams: { command: BACKGROUND_HOLD_CMD, background: true },
});
});
test("pty background exec is not killed when tool signal aborts" , async () => {
const tool = createTestExecTool({ allowBackground: true , backgroundMs: 0 });
await expectBackgroundSessionSurvivesAbort({
tool,
executeParams: { command: BACKGROUND_HOLD_CMD, background: true , pty: true },
});
});
test("background exec still times out after tool signal abort" , async () => {
const tool = createTestExecTool({ allowBackground: true , backgroundMs: 0 });
await expectBackgroundSessionTimesOut({
tool,
executeParams: {
command: BACKGROUND_HOLD_CMD,
background: true ,
timeout: BACKGROUND_TIMEOUT_SEC,
},
abortAfterStart: true ,
});
});
test("background exec without explicit timeout ignores default timeout" , async () => {
const tool = createTestExecTool({
allowBackground: true ,
backgroundMs: 0 ,
timeoutSec: BACKGROUND_TIMEOUT_SEC,
});
const result = await tool.execute("toolcall" , { command: BACKGROUND_HOLD_CMD, background: true });
expect(result.details.status).toBe("running" );
const sessionId = (result.details as { sessionId: string }).sessionId;
expect(supervisorMockState.spawnInputs.at(-1 )?.timeoutMs).toBeUndefined();
expect(getFinishedSession(sessionId)).toBeUndefined();
expect(getSession(sessionId)?.exited).toBe(false );
cleanupRunningSession(sessionId);
});
test("yielded background exec still times out" , async () => {
const tool = createTestExecTool({ allowBackground: true , backgroundMs: 10 });
await expectBackgroundSessionTimesOut({
tool,
executeParams: {
command: BACKGROUND_HOLD_CMD,
yieldMs: 5 ,
timeout: BACKGROUND_TIMEOUT_SEC,
},
});
});
Messung V0.5 in Prozent C=100 H=97 G=98
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-05-26)
¤
*© Formatika GbR, Deutschland