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: { maxStderrBytes?: number; maxStdoutBytes?: number; terminationGraceMs?: number; timeoutSec?: number; } = {}, ) => executeCliCommand(context, command, options.timeoutSec ?? 1, { apiBaseUrl: "http://127.0.0.1:8000", cliPath, maxStderrBytes: options.maxStderrBytes ?? 8, maxStdoutBytes: options.maxStdoutBytes ?? 64, terminationGraceMs: options.terminationGraceMs ?? 20, }); describe("executeCliCommand", () => { test("accepts output at the byte limit", async () => { await expect(run("stdout 123456", { maxStdoutBytes: 6 })).resolves.toMatchObject({ outcome: "completed", exitCode: 0, status: 200, stdout: "123456", }); }); test("does not treat the OpenCode 12000-byte inline threshold as a CLI limit", async () => { const stdout = "x".repeat(12_001); const result = await run(`stdout ${stdout}`, { maxStdoutBytes: 128 * 1024 * 1024 }); expect(result.outcome).toBe("completed"); expect(result.stdout).toBe(stdout); }); test("rejects multibyte output above the byte limit without returning a partial body", async () => { await expect(run("stdout 水水", { maxStdoutBytes: 5 })).resolves.toMatchObject({ outcome: "output_limit", exceededStream: "stdout", status: 502, stderr: "", stdout: "", }); }); test("truncates stderr independently without terminating a successful command", async () => { await expect( run("stderr-success 123456789", { maxStderrBytes: 6 }), ).resolves.toMatchObject({ outcome: "completed", exitCode: 0, status: 200, stderr: "123456", stderrTruncated: true, stdout: '{"ok":true}', }); }); 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, maxStderrBytes: 8, maxStdoutBytes: 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, maxStderrBytes: 8, maxStdoutBytes: 64, terminationGraceMs: 20, }), ).rejects.toBeInstanceOf(Error); }); });