test: slim pty fallback coverage

This commit is contained in:
Peter Steinberger
2026-04-24 11:19:31 +01:00
parent 01bc49c88c
commit a8edf29bd0
3 changed files with 103 additions and 99 deletions

View File

@@ -0,0 +1,103 @@
import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest";
import type { ManagedRun, SpawnInput } from "../process/supervisor/index.js";
let listRunningSessions: typeof import("./bash-process-registry.js").listRunningSessions;
let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").resetProcessRegistryForTests;
let runExecProcess: typeof import("./bash-tools.exec-runtime.js").runExecProcess;
const { supervisorSpawnMock } = vi.hoisted(() => ({
supervisorSpawnMock: vi.fn(),
}));
vi.mock("../process/supervisor/index.js", () => ({
getProcessSupervisor: () => ({
spawn: supervisorSpawnMock,
cancel: vi.fn(),
cancelScope: vi.fn(),
reconcileOrphans: vi.fn(),
getRecord: vi.fn(),
}),
}));
function createSuccessfulRun(input: SpawnInput): ManagedRun {
input.onStdout?.("ok");
return {
runId: input.runId ?? "test-run",
pid: 1234,
startedAtMs: Date.now(),
stdin: {
write: vi.fn(),
end: vi.fn(),
destroy: vi.fn(),
},
cancel: vi.fn(),
wait: vi.fn(async () => ({
reason: "exit" as const,
exitCode: 0,
exitSignal: null,
durationMs: 1,
stdout: "",
stderr: "",
timedOut: false,
noOutputTimedOut: false,
})),
};
}
beforeAll(async () => {
({ listRunningSessions, resetProcessRegistryForTests } =
await import("./bash-process-registry.js"));
({ runExecProcess } = await import("./bash-tools.exec-runtime.js"));
});
beforeEach(() => {
supervisorSpawnMock.mockReset();
});
afterEach(() => {
resetProcessRegistryForTests();
vi.clearAllMocks();
});
function runPtyFallback(warnings: string[] = []) {
return runExecProcess({
command: "printf ok",
workdir: process.cwd(),
env: {},
usePty: true,
warnings,
maxOutput: 20_000,
pendingMaxOutput: 20_000,
notifyOnExit: false,
timeoutSec: 5,
});
}
test("exec falls back when PTY spawn fails", async () => {
supervisorSpawnMock
.mockRejectedValueOnce(new Error("pty spawn failed"))
.mockImplementationOnce(async (input: SpawnInput) => createSuccessfulRun(input));
const warnings: string[] = [];
const handle = await runPtyFallback(warnings);
const outcome = await handle.promise;
expect(outcome.status).toBe("completed");
expect(outcome.aggregated).toContain("ok");
expect(warnings.join("\n")).toContain("PTY spawn failed");
expect(supervisorSpawnMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ mode: "pty" }));
expect(supervisorSpawnMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ mode: "child" }),
);
});
test("exec cleans session state when PTY fallback spawn also fails", async () => {
supervisorSpawnMock
.mockRejectedValueOnce(new Error("pty spawn failed"))
.mockRejectedValueOnce(new Error("child fallback failed"));
await expect(runPtyFallback()).rejects.toThrow("child fallback failed");
expect(listRunningSessions()).toHaveLength(0);
});

View File

@@ -1,60 +0,0 @@
import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest";
let createExecTool: typeof import("./bash-tools.exec.js").createExecTool;
let listRunningSessions: typeof import("./bash-process-registry.js").listRunningSessions;
let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").resetProcessRegistryForTests;
const { supervisorSpawnMock } = vi.hoisted(() => ({
supervisorSpawnMock: vi.fn(),
}));
const makeSupervisor = () => {
const noop = vi.fn();
return {
spawn: (...args: unknown[]) => supervisorSpawnMock(...args),
cancel: noop,
cancelScope: noop,
reconcileOrphans: noop,
getRecord: noop,
};
};
vi.mock("../process/supervisor/index.js", () => ({
getProcessSupervisor: () => makeSupervisor(),
}));
beforeAll(async () => {
({ createExecTool } = await import("./bash-tools.exec.js"));
({ listRunningSessions, resetProcessRegistryForTests } =
await import("./bash-process-registry.js"));
});
beforeEach(() => {
supervisorSpawnMock.mockReset();
});
afterEach(() => {
resetProcessRegistryForTests();
vi.clearAllMocks();
});
test("exec cleans session state when PTY fallback spawn also fails", async () => {
supervisorSpawnMock
.mockRejectedValueOnce(new Error("pty spawn failed"))
.mockRejectedValueOnce(new Error("child fallback failed"));
const tool = createExecTool({
allowBackground: false,
host: "gateway",
security: "full",
ask: "off",
});
await expect(
tool.execute("toolcall", {
command: "echo ok",
pty: true,
}),
).rejects.toThrow("child fallback failed");
expect(listRunningSessions()).toHaveLength(0);
});

View File

@@ -1,39 +0,0 @@
import { afterEach, beforeAll, expect, test, vi } from "vitest";
let createExecTool: typeof import("./bash-tools.exec.js").createExecTool;
let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").resetProcessRegistryForTests;
vi.mock("@lydell/node-pty", () => ({
spawn: () => {
const err = new Error("spawn EBADF");
(err as NodeJS.ErrnoException).code = "EBADF";
throw err;
},
}));
beforeAll(async () => {
({ createExecTool } = await import("./bash-tools.exec.js"));
({ resetProcessRegistryForTests } = await import("./bash-process-registry.js"));
});
afterEach(() => {
resetProcessRegistryForTests();
vi.clearAllMocks();
});
test("exec falls back when PTY spawn fails", async () => {
const tool = createExecTool({
allowBackground: false,
host: "gateway",
security: "full",
ask: "off",
});
const result = await tool.execute("toolcall", {
command: "printf ok",
pty: true,
});
expect(result.details.status).toBe("completed");
const text = result.content?.find((item) => item.type === "text")?.text ?? "";
expect(text).toContain("ok");
expect(text).toContain("PTY spawn failed");
});