fix(agent): bound CLI subprocess execution
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { executeCliCommand } from "../../src/cli/executeCliCommand.js";
|
||||
import { type RuntimeSessionContext } from "../../src/runtime/sessionContext.js";
|
||||
|
||||
const cliPath = fileURLToPath(
|
||||
new URL("../fixtures/fakeCli.mjs", import.meta.url),
|
||||
);
|
||||
|
||||
const context: RuntimeSessionContext = {
|
||||
accessToken: "test-token",
|
||||
actorKey: "actor-1",
|
||||
clientSessionId: "client-1",
|
||||
projectId: "project-1",
|
||||
projectKey: "project-1",
|
||||
sessionId: "session-1",
|
||||
traceId: "trace-1",
|
||||
};
|
||||
|
||||
const run = (
|
||||
command: string,
|
||||
options: {
|
||||
maxOutputBytes?: number;
|
||||
terminationGraceMs?: number;
|
||||
timeoutSec?: number;
|
||||
} = {},
|
||||
) =>
|
||||
executeCliCommand(context, command, options.timeoutSec ?? 1, {
|
||||
apiBaseUrl: "http://127.0.0.1:8000",
|
||||
cliPath,
|
||||
maxOutputBytes: options.maxOutputBytes ?? 64,
|
||||
terminationGraceMs: options.terminationGraceMs ?? 20,
|
||||
});
|
||||
|
||||
describe("executeCliCommand", () => {
|
||||
test("accepts output at the byte limit", async () => {
|
||||
await expect(run("stdout 123456", { maxOutputBytes: 6 })).resolves.toMatchObject({
|
||||
outcome: "completed",
|
||||
exitCode: 0,
|
||||
status: 200,
|
||||
stdout: "123456",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects multibyte output above the byte limit without returning a partial body", async () => {
|
||||
await expect(run("stdout 水水", { maxOutputBytes: 5 })).resolves.toMatchObject({
|
||||
outcome: "output_limit",
|
||||
exceededStream: "stdout",
|
||||
status: 502,
|
||||
stderr: "",
|
||||
stdout: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("limits stderr independently", async () => {
|
||||
await expect(run("stderr 1234567", { maxOutputBytes: 6 })).resolves.toMatchObject({
|
||||
outcome: "output_limit",
|
||||
exceededStream: "stderr",
|
||||
status: 502,
|
||||
stderr: "",
|
||||
stdout: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("waits for a SIGTERM-aware process to close after timeout", async () => {
|
||||
const startedAt = Date.now();
|
||||
const result = await run("term", {
|
||||
terminationGraceMs: 100,
|
||||
timeoutSec: 0.25,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ outcome: "timeout", status: 504 });
|
||||
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(270);
|
||||
});
|
||||
|
||||
test("uses SIGKILL when a timed-out process ignores SIGTERM", async () => {
|
||||
const result = await run("ignore-term", { timeoutSec: 0.25 });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
outcome: "timeout",
|
||||
signal: "SIGKILL",
|
||||
status: 504,
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the timeout outcome when closing stdin also errors", async () => {
|
||||
const largeContext = {
|
||||
...context,
|
||||
accessToken: "x".repeat(1024 * 1024),
|
||||
};
|
||||
|
||||
await expect(
|
||||
executeCliCommand(largeContext, "ignore-term", 0.25, {
|
||||
apiBaseUrl: "http://127.0.0.1:8000",
|
||||
cliPath,
|
||||
maxOutputBytes: 64,
|
||||
terminationGraceMs: 20,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
outcome: "timeout",
|
||||
signal: "SIGKILL",
|
||||
status: 504,
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects a deterministic stdin pipe error without crashing", async () => {
|
||||
const largeContext = {
|
||||
...context,
|
||||
accessToken: "x".repeat(1024 * 1024),
|
||||
};
|
||||
|
||||
await expect(
|
||||
executeCliCommand(largeContext, "closed-stdin", 1, {
|
||||
apiBaseUrl: "http://127.0.0.1:8000",
|
||||
cliPath,
|
||||
maxOutputBytes: 64,
|
||||
terminationGraceMs: 20,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user