test: stabilize bash e2e suites with explicit exec approvals mode

This commit is contained in:
Peter Steinberger
2026-02-22 10:37:44 +00:00
parent 97eb4af01e
commit c68bb8d6d5
5 changed files with 35 additions and 19 deletions

View File

@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js";
import { captureEnv } from "../test-utils/env.js";
import { getFinishedSession, resetProcessRegistryForTests } from "./bash-process-registry.js";
import { createExecTool, createProcessTool, execTool, processTool } from "./bash-tools.js";
import { createExecTool, createProcessTool } from "./bash-tools.js";
import { buildDockerExecArgs } from "./bash-tools.shared.js";
import { resolveShellFromPath, sanitizeBinaryOutput } from "./shell-utils.js";
@@ -16,6 +16,12 @@ const shortDelayCmd = isWin ? "Start-Sleep -Milliseconds 20" : "sleep 0.02";
const yieldDelayCmd = isWin ? "Start-Sleep -Milliseconds 90" : "sleep 0.09";
const longDelayCmd = isWin ? "Start-Sleep -Milliseconds 700" : "sleep 0.7";
const POLL_INTERVAL_MS = 15;
const TEST_EXEC_DEFAULTS = { security: "full" as const, ask: "off" as const };
const createTestExecTool = (
defaults?: Parameters<typeof createExecTool>[0],
): ReturnType<typeof createExecTool> => createExecTool({ ...TEST_EXEC_DEFAULTS, ...defaults });
const execTool = createTestExecTool();
const processTool = createProcessTool();
// Both PowerShell and bash use ; for command separation
const joinCommands = (commands: string[]) => commands.join("; ");
const echoAfterDelay = (message: string) => joinCommands([shortDelayCmd, `echo ${message}`]);
@@ -144,7 +150,7 @@ describe("exec tool backgrounding", () => {
});
it("uses default timeout when timeout is omitted", async () => {
const customBash = createExecTool({ timeoutSec: 0.1, backgroundMs: 10 });
const customBash = createTestExecTool({ timeoutSec: 0.1, backgroundMs: 10 });
const customProcess = createProcessTool();
const result = await customBash.execute("call1", {
@@ -168,7 +174,7 @@ describe("exec tool backgrounding", () => {
});
it("rejects elevated requests when not allowed", async () => {
const customBash = createExecTool({
const customBash = createTestExecTool({
elevated: { enabled: true, allowed: false, defaultLevel: "off" },
messageProvider: "telegram",
sessionKey: "agent:main:main",
@@ -183,7 +189,7 @@ describe("exec tool backgrounding", () => {
});
it("does not default to elevated when not allowed", async () => {
const customBash = createExecTool({
const customBash = createTestExecTool({
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
backgroundMs: 1000,
timeoutSec: 5,
@@ -270,9 +276,9 @@ describe("exec tool backgrounding", () => {
});
it("scopes process sessions by scopeKey", async () => {
const bashA = createExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
const bashA = createTestExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
const processA = createProcessTool({ scopeKey: "agent:alpha" });
const bashB = createExecTool({ backgroundMs: 10, scopeKey: "agent:beta" });
const bashB = createTestExecTool({ backgroundMs: 10, scopeKey: "agent:beta" });
const processB = createProcessTool({ scopeKey: "agent:beta" });
const resultA = await bashA.execute("call1", {
@@ -332,7 +338,7 @@ describe("exec exit codes", () => {
describe("exec notifyOnExit", () => {
it("enqueues a system event when a backgrounded exec exits", async () => {
const tool = createExecTool({
const tool = createTestExecTool({
allowBackground: true,
backgroundMs: 0,
notifyOnExit: true,
@@ -372,7 +378,7 @@ describe("exec notifyOnExit", () => {
});
it("skips no-op completion events when command succeeds without output", async () => {
const tool = createExecTool({
const tool = createTestExecTool({
allowBackground: true,
backgroundMs: 0,
notifyOnExit: true,
@@ -392,7 +398,7 @@ describe("exec notifyOnExit", () => {
});
it("can re-enable no-op completion events via notifyOnExitEmptySuccess", async () => {
const tool = createExecTool({
const tool = createTestExecTool({
allowBackground: true,
backgroundMs: 0,
notifyOnExit: true,
@@ -434,13 +440,15 @@ describe("exec PATH handling", () => {
const prepend = isWin ? ["C:\\custom\\bin", "C:\\oss\\bin"] : ["/custom/bin", "/opt/oss/bin"];
process.env.PATH = basePath;
const tool = createExecTool({ pathPrepend: prepend });
const tool = createTestExecTool({ pathPrepend: prepend });
const result = await tool.execute("call1", {
command: isWin ? "Write-Output $env:PATH" : "echo $PATH",
});
const text = normalizeText(result.content.find((c) => c.type === "text")?.text);
expect(text).toBe([...prepend, basePath].join(path.delimiter));
const entries = text.split(path.delimiter);
expect(entries.slice(0, prepend.length)).toEqual(prepend);
expect(entries).toContain(basePath);
});
});

View File

@@ -13,6 +13,14 @@ const ABORT_WAIT_TIMEOUT_MS = process.platform === "win32" ? 1_500 : 450;
const POLL_INTERVAL_MS = 15;
const FINISHED_WAIT_TIMEOUT_MS = process.platform === "win32" ? 8_000 : 1_200;
const BACKGROUND_TIMEOUT_SEC = process.platform === "win32" ? 0.2 : 0.12;
const TEST_EXEC_DEFAULTS = {
security: "full" as const,
ask: "off" as const,
};
const createTestExecTool = (
defaults?: Parameters<typeof createExecTool>[0],
): ReturnType<typeof createExecTool> => createExecTool({ ...TEST_EXEC_DEFAULTS, ...defaults });
afterEach(() => {
resetProcessRegistryForTests();
@@ -106,7 +114,7 @@ async function expectBackgroundSessionTimesOut(params: {
}
test("background exec is not killed when tool signal aborts", async () => {
const tool = createExecTool({ allowBackground: true, backgroundMs: 0 });
const tool = createTestExecTool({ allowBackground: true, backgroundMs: 0 });
await expectBackgroundSessionSurvivesAbort({
tool,
executeParams: { command: BACKGROUND_HOLD_CMD, background: true },
@@ -114,7 +122,7 @@ test("background exec is not killed when tool signal aborts", async () => {
});
test("pty background exec is not killed when tool signal aborts", async () => {
const tool = createExecTool({ allowBackground: true, backgroundMs: 0 });
const tool = createTestExecTool({ allowBackground: true, backgroundMs: 0 });
await expectBackgroundSessionSurvivesAbort({
tool,
executeParams: { command: BACKGROUND_HOLD_CMD, background: true, pty: true },
@@ -122,7 +130,7 @@ test("pty background exec is not killed when tool signal aborts", async () => {
});
test("background exec still times out after tool signal abort", async () => {
const tool = createExecTool({ allowBackground: true, backgroundMs: 0 });
const tool = createTestExecTool({ allowBackground: true, backgroundMs: 0 });
await expectBackgroundSessionTimesOut({
tool,
executeParams: {
@@ -135,7 +143,7 @@ test("background exec still times out after tool signal abort", async () => {
});
test("yielded background exec is not killed when tool signal aborts", async () => {
const tool = createExecTool({ allowBackground: true, backgroundMs: 10 });
const tool = createTestExecTool({ allowBackground: true, backgroundMs: 10 });
await expectBackgroundSessionSurvivesAbort({
tool,
executeParams: { command: BACKGROUND_HOLD_CMD, yieldMs: 5 },
@@ -143,7 +151,7 @@ test("yielded background exec is not killed when tool signal aborts", async () =
});
test("yielded background exec still times out", async () => {
const tool = createExecTool({ allowBackground: true, backgroundMs: 10 });
const tool = createTestExecTool({ allowBackground: true, backgroundMs: 10 });
await expectBackgroundSessionTimesOut({
tool,
executeParams: {

View File

@@ -16,7 +16,7 @@ afterEach(() => {
});
test("exec falls back when PTY spawn fails", async () => {
const tool = createExecTool({ allowBackground: false });
const tool = createExecTool({ allowBackground: false, security: "full", ask: "off" });
const result = await tool.execute("toolcall", {
command: "printf ok",
pty: true,

View File

@@ -7,7 +7,7 @@ afterEach(() => {
});
test("exec supports pty output", async () => {
const tool = createExecTool({ allowBackground: false });
const tool = createExecTool({ allowBackground: false, security: "full", ask: "off" });
const result = await tool.execute("toolcall", {
command: 'node -e "process.stdout.write(String.fromCharCode(111,107))"',
pty: true,

View File

@@ -8,7 +8,7 @@ afterEach(() => {
});
async function startPtySession(command: string) {
const execTool = createExecTool();
const execTool = createExecTool({ security: "full", ask: "off" });
const processTool = createProcessTool();
const result = await execTool.execute("toolcall", {
command,