diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 01:09:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 01:09:39 +0900 |
| commit | 61e45e60d699ed1ca46f94a8f181c92a940317c6 (patch) | |
| tree | 2892d9773c5a8e367e1e58cdb1e88d9c6ad3fe6d /packages/tool-shell/src/shell.test.ts | |
| parent | 63c7e64532e85e0bbdd6d9ac6825d8f86be98e7a (diff) | |
| parent | 727c98c9dae516a2070eb950410314380a20c974 (diff) | |
| download | dispatch-61e45e60d699ed1ca46f94a8f181c92a940317c6.tar.gz dispatch-61e45e60d699ed1ca46f94a8f181c92a940317c6.zip | |
Merge branch 'feature/indent-change' into dev
Diffstat (limited to 'packages/tool-shell/src/shell.test.ts')
| -rw-r--r-- | packages/tool-shell/src/shell.test.ts | 816 |
1 files changed, 408 insertions, 408 deletions
diff --git a/packages/tool-shell/src/shell.test.ts b/packages/tool-shell/src/shell.test.ts index 4a579b0..42127e6 100644 --- a/packages/tool-shell/src/shell.test.ts +++ b/packages/tool-shell/src/shell.test.ts @@ -4,436 +4,436 @@ import { describe, expect, it } from "vitest"; import { buildResult, createRunShellTool, truncateOutput, validateArgs } from "./shell.js"; function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext { - return { - toolCallId: "test-call-1", - onOutput: () => {}, - signal: AbortSignal.timeout(5000), - log: createLogger( - { extensionId: "test" }, - { emit: () => {} }, - { now: () => 0, newId: () => "id" }, - ), - ...overrides, - }; + return { + toolCallId: "test-call-1", + onOutput: () => {}, + signal: AbortSignal.timeout(5000), + log: createLogger( + { extensionId: "test" }, + { emit: () => {} }, + { now: () => 0, newId: () => "id" }, + ), + ...overrides, + }; } /** A fake backend whose `spawn` resolves a fixed result (no real I/O). */ function fakeBackend(result: { - exitCode: number | null; - timedOut: boolean; - aborted?: boolean; + exitCode: number | null; + timedOut: boolean; + aborted?: boolean; }): ExecBackend { - return { - ...localExecBackend, - spawn: async () => ({ aborted: false, ...result }), - }; + return { + ...localExecBackend, + spawn: async () => ({ aborted: false, ...result }), + }; } /** Wrap a fake backend in the resolver the factory expects (computerId-agnostic). */ function resolverFor(backend: ExecBackend) { - return () => backend; + return () => backend; } describe("validateArgs", () => { - it("returns validated args for valid input", () => { - const result = validateArgs({ command: "echo hello" }); - expect(result).toEqual({ command: "echo hello", timeout: 120_000 }); - }); - - it("parses custom timeout", () => { - const result = validateArgs({ command: "echo hello", timeout: 5000 }); - expect(result).toEqual({ command: "echo hello", timeout: 5000 }); - }); - - it("floors fractional timeout", () => { - const result = validateArgs({ command: "echo hello", timeout: 5000.7 }); - expect(result).toEqual({ command: "echo hello", timeout: 5000 }); - }); - - it("returns error for null args", () => { - const result = validateArgs(null); - expect(result).toHaveProperty("error"); - }); - - it("returns error for non-object args", () => { - const result = validateArgs("string"); - expect(result).toHaveProperty("error"); - }); - - it("returns error for missing command", () => { - const result = validateArgs({}); - expect(result).toHaveProperty("error"); - }); - - it("rejects missing or empty command", () => { - const empty = validateArgs({ command: "" }); - expect(empty).toHaveProperty("error"); - const whitespace = validateArgs({ command: " " }); - expect(whitespace).toHaveProperty("error"); - const missing = validateArgs({ timeout: 5000 }); - expect(missing).toHaveProperty("error"); - }); - - it("returns error for non-string command", () => { - const result = validateArgs({ command: 123 }); - expect(result).toHaveProperty("error"); - }); - - it("returns error for invalid timeout", () => { - const negative = validateArgs({ command: "echo", timeout: -1 }); - expect(negative).toHaveProperty("error"); - const zero = validateArgs({ command: "echo", timeout: 0 }); - expect(zero).toHaveProperty("error"); - const nan = validateArgs({ command: "echo", timeout: Number.NaN }); - expect(nan).toHaveProperty("error"); - }); + it("returns validated args for valid input", () => { + const result = validateArgs({ command: "echo hello" }); + expect(result).toEqual({ command: "echo hello", timeout: 120_000 }); + }); + + it("parses custom timeout", () => { + const result = validateArgs({ command: "echo hello", timeout: 5000 }); + expect(result).toEqual({ command: "echo hello", timeout: 5000 }); + }); + + it("floors fractional timeout", () => { + const result = validateArgs({ command: "echo hello", timeout: 5000.7 }); + expect(result).toEqual({ command: "echo hello", timeout: 5000 }); + }); + + it("returns error for null args", () => { + const result = validateArgs(null); + expect(result).toHaveProperty("error"); + }); + + it("returns error for non-object args", () => { + const result = validateArgs("string"); + expect(result).toHaveProperty("error"); + }); + + it("returns error for missing command", () => { + const result = validateArgs({}); + expect(result).toHaveProperty("error"); + }); + + it("rejects missing or empty command", () => { + const empty = validateArgs({ command: "" }); + expect(empty).toHaveProperty("error"); + const whitespace = validateArgs({ command: " " }); + expect(whitespace).toHaveProperty("error"); + const missing = validateArgs({ timeout: 5000 }); + expect(missing).toHaveProperty("error"); + }); + + it("returns error for non-string command", () => { + const result = validateArgs({ command: 123 }); + expect(result).toHaveProperty("error"); + }); + + it("returns error for invalid timeout", () => { + const negative = validateArgs({ command: "echo", timeout: -1 }); + expect(negative).toHaveProperty("error"); + const zero = validateArgs({ command: "echo", timeout: 0 }); + expect(zero).toHaveProperty("error"); + const nan = validateArgs({ command: "echo", timeout: Number.NaN }); + expect(nan).toHaveProperty("error"); + }); }); describe("truncateOutput", () => { - it("returns output unchanged when under cap", () => { - const output = "short output"; - expect(truncateOutput(output, 100)).toBe("short output"); - }); - - it("returns output unchanged when exactly at cap", () => { - const output = "exact"; - expect(truncateOutput(output, 5)).toBe("exact"); - }); - - it("truncates output beyond the cap and appends a notice", () => { - const output = "a".repeat(100); - const result = truncateOutput(output, 50); - expect(result).toContain("a".repeat(50)); - expect(result).toContain("[Output truncated: exceeded 50 characters]"); - expect(result.length).toBeLessThan(output.length + 100); - }); + it("returns output unchanged when under cap", () => { + const output = "short output"; + expect(truncateOutput(output, 100)).toBe("short output"); + }); + + it("returns output unchanged when exactly at cap", () => { + const output = "exact"; + expect(truncateOutput(output, 5)).toBe("exact"); + }); + + it("truncates output beyond the cap and appends a notice", () => { + const output = "a".repeat(100); + const result = truncateOutput(output, 50); + expect(result).toContain("a".repeat(50)); + expect(result).toContain("[Output truncated: exceeded 50 characters]"); + expect(result.length).toBeLessThan(output.length + 100); + }); }); describe("buildResult", () => { - it("maps a zero exit code to a success result", () => { - const result = buildResult({ - exitCode: 0, - timedOut: false, - aborted: false, - output: "all good", - cap: 50_000, - }); - expect(result.content).toBe("all good"); - expect(result.isError).toBeUndefined(); - }); - - it("maps a non-zero exit code to an isError result", () => { - const result = buildResult({ - exitCode: 1, - timedOut: false, - aborted: false, - output: "some error", - cap: 50_000, - }); - expect(result.content).toBe("some error"); - expect(result.isError).toBe(true); - }); - - it("reports a timeout as an isError result", () => { - const result = buildResult({ - exitCode: null, - timedOut: true, - aborted: false, - output: "partial", - cap: 50_000, - }); - expect(result.content).toContain("partial"); - expect(result.content).toContain("[Command timed out]"); - expect(result.isError).toBe(true); - }); - - it("reports abort as an isError result", () => { - const result = buildResult({ - exitCode: null, - timedOut: false, - aborted: true, - output: "interrupted", - cap: 50_000, - }); - expect(result.content).toBe("interrupted"); - expect(result.isError).toBe(true); - }); - - it("truncates output in result when over cap", () => { - const output = "x".repeat(60_000); - const result = buildResult({ - exitCode: 0, - timedOut: false, - aborted: false, - output, - cap: 50_000, - }); - expect(result.content).toContain("[Output truncated"); - expect(result.content.length).toBeLessThan(60_000); - }); + it("maps a zero exit code to a success result", () => { + const result = buildResult({ + exitCode: 0, + timedOut: false, + aborted: false, + output: "all good", + cap: 50_000, + }); + expect(result.content).toBe("all good"); + expect(result.isError).toBeUndefined(); + }); + + it("maps a non-zero exit code to an isError result", () => { + const result = buildResult({ + exitCode: 1, + timedOut: false, + aborted: false, + output: "some error", + cap: 50_000, + }); + expect(result.content).toBe("some error"); + expect(result.isError).toBe(true); + }); + + it("reports a timeout as an isError result", () => { + const result = buildResult({ + exitCode: null, + timedOut: true, + aborted: false, + output: "partial", + cap: 50_000, + }); + expect(result.content).toContain("partial"); + expect(result.content).toContain("[Command timed out]"); + expect(result.isError).toBe(true); + }); + + it("reports abort as an isError result", () => { + const result = buildResult({ + exitCode: null, + timedOut: false, + aborted: true, + output: "interrupted", + cap: 50_000, + }); + expect(result.content).toBe("interrupted"); + expect(result.isError).toBe(true); + }); + + it("truncates output in result when over cap", () => { + const output = "x".repeat(60_000); + const result = buildResult({ + exitCode: 0, + timedOut: false, + aborted: false, + output, + cap: 50_000, + }); + expect(result.content).toContain("[Output truncated"); + expect(result.content.length).toBeLessThan(60_000); + }); }); describe("createRunShellTool", () => { - it("has correct name and parameters shape", () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), - }); - expect(tool.name).toBe("run_shell"); - expect(tool.parameters.type).toBe("object"); - expect(tool.parameters.required).toEqual(["command"]); - expect(tool.parameters.properties?.command?.type).toBe("string"); - expect(tool.parameters.properties?.timeout?.type).toBe("number"); - }); - - it("concurrencySafe is false", () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), - }); - expect(tool.concurrencySafe).toBe(false); - }); - - it("rejects missing or empty command", async () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), - }); - const result = await tool.execute({}, stubCtx()); - expect(result.isError).toBe(true); - expect(result.content).toContain("Missing or empty"); - }); - - it("maps a zero exit code to a success result", async () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - params.onOutput("hello\n", "stdout"); - return { exitCode: 0, timedOut: false, aborted: false }; - }, - }), - }); - const result = await tool.execute({ command: "echo hello" }, stubCtx()); - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("hello"); - }); - - it("maps a non-zero exit code to an isError result", async () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - params.onOutput("error output\n", "stderr"); - return { exitCode: 1, timedOut: false, aborted: false }; - }, - }), - }); - const result = await tool.execute({ command: "false" }, stubCtx()); - expect(result.isError).toBe(true); - expect(result.content).toContain("error output"); - }); - - it("reports a timeout as an isError result", async () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - params.onOutput("partial\n", "stdout"); - return { exitCode: null, timedOut: true, aborted: false }; - }, - }), - }); - const result = await tool.execute({ command: "sleep 999" }, stubCtx()); - expect(result.isError).toBe(true); - expect(result.content).toContain("[Command timed out]"); - }); - - it("truncates output beyond the cap and appends a notice", async () => { - const cap = 100; - const tool = createRunShellTool({ - workdir: "/tmp", - outputCap: cap, - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - params.onOutput("a".repeat(200), "stdout"); - return { exitCode: 0, timedOut: false, aborted: false }; - }, - }), - }); - const result = await tool.execute({ command: "gen" }, stubCtx()); - expect(result.content).toContain("[Output truncated"); - expect(result.content.length).toBeLessThan(200); - }); - - it("streams output to ctx.onOutput", async () => { - const chunks: Array<{ data: string; stream: "stdout" | "stderr" }> = []; - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - params.onOutput("line1\n", "stdout"); - params.onOutput("err1\n", "stderr"); - params.onOutput("line2\n", "stdout"); - return { exitCode: 0, timedOut: false, aborted: false }; - }, - }), - }); - await tool.execute( - { command: "test" }, - stubCtx({ - onOutput: (data, stream) => chunks.push({ data, stream }), - }), - ); - expect(chunks).toEqual([ - { data: "line1\n", stream: "stdout" }, - { data: "err1\n", stream: "stderr" }, - { data: "line2\n", stream: "stdout" }, - ]); - }); - - it("uses ctx.cwd when present over baked workdir", async () => { - let receivedCwd = ""; - const tool = createRunShellTool({ - workdir: "/baked", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - receivedCwd = params.cwd; - return { exitCode: 0, timedOut: false, aborted: false }; - }, - }), - }); - await tool.execute({ command: "pwd" }, stubCtx({ cwd: "/custom" })); - expect(receivedCwd).toBe("/custom"); - }); - - it("falls back to baked workdir when ctx.cwd is omitted", async () => { - let receivedCwd = ""; - const tool = createRunShellTool({ - workdir: "/baked", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - receivedCwd = params.cwd; - return { exitCode: 0, timedOut: false, aborted: false }; - }, - }), - }); - await tool.execute({ command: "pwd" }, stubCtx()); - expect(receivedCwd).toBe("/baked"); - }); - - it("returns error for spawn failure", async () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async () => { - throw new Error("spawn failed"); - }, - }), - }); - const result = await tool.execute({ command: "bad" }, stubCtx()); - expect(result.isError).toBe(true); - expect(result.content).toContain("Error spawning command"); - }); - - it("reports abort as isError when signal fires before spawn completes", async () => { - const controller = new AbortController(); - controller.abort(); - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), - }); - const result = await tool.execute({ command: "test" }, stubCtx({ signal: controller.signal })); - expect(result.isError).toBe(true); - }); - - it("passes timeout to spawn", async () => { - let receivedTimeout = 0; - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: resolverFor({ - ...localExecBackend, - spawn: async (params) => { - receivedTimeout = params.timeout; - return { exitCode: 0, timedOut: false, aborted: false }; - }, - }), - }); - await tool.execute({ command: "test", timeout: 5000 }, stubCtx()); - expect(receivedTimeout).toBe(5000); - }); - - it("resolves the backend per-call from ctx.computerId (passes it to the resolver)", async () => { - let receivedComputerId: string | undefined = "__unset__"; - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: (computerId) => { - receivedComputerId = computerId; - return fakeBackend({ exitCode: 0, timedOut: false }); - }, - }); - await tool.execute({ command: "test" }, stubCtx({ computerId: "my-host" })); - expect(receivedComputerId).toBe("my-host"); - }); - - it("resolves the local backend when ctx.computerId is undefined", async () => { - let receivedComputerId: string | undefined = "__unset__"; - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: (computerId) => { - receivedComputerId = computerId; - return fakeBackend({ exitCode: 0, timedOut: false }); - }, - }); - await tool.execute({ command: "test" }, stubCtx()); - expect(receivedComputerId).toBeUndefined(); - }); + it("has correct name and parameters shape", () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), + }); + expect(tool.name).toBe("run_shell"); + expect(tool.parameters.type).toBe("object"); + expect(tool.parameters.required).toEqual(["command"]); + expect(tool.parameters.properties?.command?.type).toBe("string"); + expect(tool.parameters.properties?.timeout?.type).toBe("number"); + }); + + it("concurrencySafe is false", () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), + }); + expect(tool.concurrencySafe).toBe(false); + }); + + it("rejects missing or empty command", async () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), + }); + const result = await tool.execute({}, stubCtx()); + expect(result.isError).toBe(true); + expect(result.content).toContain("Missing or empty"); + }); + + it("maps a zero exit code to a success result", async () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + params.onOutput("hello\n", "stdout"); + return { exitCode: 0, timedOut: false, aborted: false }; + }, + }), + }); + const result = await tool.execute({ command: "echo hello" }, stubCtx()); + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("hello"); + }); + + it("maps a non-zero exit code to an isError result", async () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + params.onOutput("error output\n", "stderr"); + return { exitCode: 1, timedOut: false, aborted: false }; + }, + }), + }); + const result = await tool.execute({ command: "false" }, stubCtx()); + expect(result.isError).toBe(true); + expect(result.content).toContain("error output"); + }); + + it("reports a timeout as an isError result", async () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + params.onOutput("partial\n", "stdout"); + return { exitCode: null, timedOut: true, aborted: false }; + }, + }), + }); + const result = await tool.execute({ command: "sleep 999" }, stubCtx()); + expect(result.isError).toBe(true); + expect(result.content).toContain("[Command timed out]"); + }); + + it("truncates output beyond the cap and appends a notice", async () => { + const cap = 100; + const tool = createRunShellTool({ + workdir: "/tmp", + outputCap: cap, + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + params.onOutput("a".repeat(200), "stdout"); + return { exitCode: 0, timedOut: false, aborted: false }; + }, + }), + }); + const result = await tool.execute({ command: "gen" }, stubCtx()); + expect(result.content).toContain("[Output truncated"); + expect(result.content.length).toBeLessThan(200); + }); + + it("streams output to ctx.onOutput", async () => { + const chunks: Array<{ data: string; stream: "stdout" | "stderr" }> = []; + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + params.onOutput("line1\n", "stdout"); + params.onOutput("err1\n", "stderr"); + params.onOutput("line2\n", "stdout"); + return { exitCode: 0, timedOut: false, aborted: false }; + }, + }), + }); + await tool.execute( + { command: "test" }, + stubCtx({ + onOutput: (data, stream) => chunks.push({ data, stream }), + }), + ); + expect(chunks).toEqual([ + { data: "line1\n", stream: "stdout" }, + { data: "err1\n", stream: "stderr" }, + { data: "line2\n", stream: "stdout" }, + ]); + }); + + it("uses ctx.cwd when present over baked workdir", async () => { + let receivedCwd = ""; + const tool = createRunShellTool({ + workdir: "/baked", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + receivedCwd = params.cwd; + return { exitCode: 0, timedOut: false, aborted: false }; + }, + }), + }); + await tool.execute({ command: "pwd" }, stubCtx({ cwd: "/custom" })); + expect(receivedCwd).toBe("/custom"); + }); + + it("falls back to baked workdir when ctx.cwd is omitted", async () => { + let receivedCwd = ""; + const tool = createRunShellTool({ + workdir: "/baked", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + receivedCwd = params.cwd; + return { exitCode: 0, timedOut: false, aborted: false }; + }, + }), + }); + await tool.execute({ command: "pwd" }, stubCtx()); + expect(receivedCwd).toBe("/baked"); + }); + + it("returns error for spawn failure", async () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async () => { + throw new Error("spawn failed"); + }, + }), + }); + const result = await tool.execute({ command: "bad" }, stubCtx()); + expect(result.isError).toBe(true); + expect(result.content).toContain("Error spawning command"); + }); + + it("reports abort as isError when signal fires before spawn completes", async () => { + const controller = new AbortController(); + controller.abort(); + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor(fakeBackend({ exitCode: 0, timedOut: false })), + }); + const result = await tool.execute({ command: "test" }, stubCtx({ signal: controller.signal })); + expect(result.isError).toBe(true); + }); + + it("passes timeout to spawn", async () => { + let receivedTimeout = 0; + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: resolverFor({ + ...localExecBackend, + spawn: async (params) => { + receivedTimeout = params.timeout; + return { exitCode: 0, timedOut: false, aborted: false }; + }, + }), + }); + await tool.execute({ command: "test", timeout: 5000 }, stubCtx()); + expect(receivedTimeout).toBe(5000); + }); + + it("resolves the backend per-call from ctx.computerId (passes it to the resolver)", async () => { + let receivedComputerId: string | undefined = "__unset__"; + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: (computerId) => { + receivedComputerId = computerId; + return fakeBackend({ exitCode: 0, timedOut: false }); + }, + }); + await tool.execute({ command: "test" }, stubCtx({ computerId: "my-host" })); + expect(receivedComputerId).toBe("my-host"); + }); + + it("resolves the local backend when ctx.computerId is undefined", async () => { + let receivedComputerId: string | undefined = "__unset__"; + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: (computerId) => { + receivedComputerId = computerId; + return fakeBackend({ exitCode: 0, timedOut: false }); + }, + }); + await tool.execute({ command: "test" }, stubCtx()); + expect(receivedComputerId).toBeUndefined(); + }); }); describe("createRunShellTool (integration)", () => { - it("runs a real echo command through the local backend and captures stdout + cwd", async () => { - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: () => localExecBackend, - }); - let streamed = ""; - const result = await tool.execute( - { command: "echo hello-from-shell" }, - stubCtx({ - onOutput: (data) => { - streamed += data; - }, - }), - ); - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("hello-from-shell"); - expect(streamed).toContain("hello-from-shell"); - }); - - it("aborts a real long-running command through the local backend and resolves with aborted", async () => { - const controller = new AbortController(); - const tool = createRunShellTool({ - workdir: "/tmp", - resolveBackend: () => localExecBackend, - }); - const promise = tool.execute({ command: "sleep 30" }, stubCtx({ signal: controller.signal })); - // Let the sleep actually start. - await new Promise((r) => setTimeout(r, 300)); - controller.abort(); - const result = await promise; - expect(result.isError).toBe(true); - // The detailed process-group-kill semantics (grandchild holding the pipes, - // prompt resolution) are owned by @dispatch/exec-backend's local.test.ts — - // realSpawn was ported there byte-for-byte. Here we only confirm the tool - // wires the backend's aborted result through to an isError result. - await new Promise((r) => setTimeout(r, 200)); - }); + it("runs a real echo command through the local backend and captures stdout + cwd", async () => { + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: () => localExecBackend, + }); + let streamed = ""; + const result = await tool.execute( + { command: "echo hello-from-shell" }, + stubCtx({ + onOutput: (data) => { + streamed += data; + }, + }), + ); + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("hello-from-shell"); + expect(streamed).toContain("hello-from-shell"); + }); + + it("aborts a real long-running command through the local backend and resolves with aborted", async () => { + const controller = new AbortController(); + const tool = createRunShellTool({ + workdir: "/tmp", + resolveBackend: () => localExecBackend, + }); + const promise = tool.execute({ command: "sleep 30" }, stubCtx({ signal: controller.signal })); + // Let the sleep actually start. + await new Promise((r) => setTimeout(r, 300)); + controller.abort(); + const result = await promise; + expect(result.isError).toBe(true); + // The detailed process-group-kill semantics (grandchild holding the pipes, + // prompt resolution) are owned by @dispatch/exec-backend's local.test.ts — + // realSpawn was ported there byte-for-byte. Here we only confirm the tool + // wires the backend's aborted result through to an isError result. + await new Promise((r) => setTimeout(r, 200)); + }); }); |
