Download src/agents/sessions/tools/bash.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 6.54 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/sessions/tools/bash.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/sessions/tools/bash.test.ts
-
curl -L -o bash.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/sessions/tools/bash.test.ts
6.54 kB
| import { readFile, rm } from "node:fs/promises"; | |
| import path from "node:path"; | |
| // Bash tool helper tests cover conversion from model-facing timeout seconds to | |
| // timer-safe millisecond values. | |
| import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; | |
| import { describe, expect, it, vi } from "vitest"; | |
| import { buildShellCommandInvocation } from "../../shell-utils.js"; | |
| import { | |
| expectNativeBashSpill, | |
| nativeBashSpillScenarios, | |
| } from "../bash-output-spill.test-support.js"; | |
| import type { BashOperations } from "./bash-operations.js"; | |
| import { createBashTool, createLocalBashOperations } from "./bash.js"; | |
| import { resolveBashTimeoutMs } from "./bash.test-support.js"; | |
| describe("bash tool timeout helpers", () => { | |
| it("converts positive timeout seconds to timer-safe milliseconds", () => { | |
| expect(resolveBashTimeoutMs(1)).toBe(1_000); | |
| expect(resolveBashTimeoutMs(1.5)).toBe(1_500); | |
| expect(resolveBashTimeoutMs(0.0005)).toBe(1); | |
| }); | |
| it("caps oversized timeout seconds", () => { | |
| // Node timers cannot safely represent arbitrary user-provided seconds. | |
| expect(resolveBashTimeoutMs(Number.MAX_SAFE_INTEGER)).toBe(MAX_TIMER_TIMEOUT_MS); | |
| }); | |
| it("allows an absent timeout", () => { | |
| expect(resolveBashTimeoutMs(undefined)).toBeUndefined(); | |
| }); | |
| it.each([Number.NaN, 0, -1])("rejects invalid timeout %s", (timeout) => { | |
| expect(() => resolveBashTimeoutMs(timeout)).toThrow( | |
| "Invalid timeout: must be a positive finite number of seconds", | |
| ); | |
| }); | |
| it.each([Number.NaN, 0, -1])("rejects invalid timeout %s before execution", async (timeout) => { | |
| const exec = vi.fn<BashOperations["exec"]>(); | |
| const tool = createBashTool(process.cwd(), { operations: { exec } }); | |
| await expect( | |
| tool.execute("call-invalid-timeout", { command: "echo ok", timeout }), | |
| ).rejects.toThrow("Invalid timeout: must be a positive finite number of seconds"); | |
| expect(exec).not.toHaveBeenCalled(); | |
| }); | |
| it("omits the command argv and supplies stdin for legacy WSL bash", () => { | |
| expect( | |
| buildShellCommandInvocation("printf ready", { | |
| shell: "C:\\Windows\\System32\\bash.exe", | |
| args: ["-s"], | |
| commandTransport: "stdin", | |
| }), | |
| ).toEqual({ | |
| argv: ["C:\\Windows\\System32\\bash.exe", "-s"], | |
| input: "printf ready", | |
| stdin: "pipe", | |
| }); | |
| }); | |
| }); | |
| describe("bash tool output lifecycle", () => { | |
| it("reports a long final line's tail and size after its newline", async () => { | |
| const text = `${"x".repeat(250_000)}END-MARKER\n`; | |
| const operations: BashOperations = { | |
| exec: async (_command, _cwd, { onData }) => { | |
| onData(Buffer.from(text), "stdout"); | |
| return { exitCode: 0 }; | |
| }, | |
| }; | |
| const result = await createBashTool(process.cwd(), { operations }).execute("long-line", { | |
| command: "ignored", | |
| }); | |
| const details = result.details; | |
| if ( | |
| !details || | |
| typeof details !== "object" || | |
| !("fullOutputPath" in details) || | |
| typeof details.fullOutputPath !== "string" | |
| ) { | |
| throw new Error("Expected a full output path for truncated Bash output"); | |
| } | |
| const fullOutputPath = details.fullOutputPath; | |
| try { | |
| expect(result.content[0]).toMatchObject({ | |
| type: "text", | |
| text: expect.stringContaining( | |
| "END-MARKER\n\n[Showing last 50.0KB of line 1 (line is 244.2KB).", | |
| ), | |
| }); | |
| expect(await readFile(fullOutputPath, "utf8")).toBe(text); | |
| } finally { | |
| await rm(fullOutputPath, { force: true }); | |
| } | |
| }); | |
| it.runIf(process.platform !== "win32").each(nativeBashSpillScenarios)( | |
| "settles real Bash output for %s", | |
| async (scenario) => { | |
| await expectNativeBashSpill("tool", scenario); | |
| }, | |
| ); | |
| it.runIf(process.platform !== "win32")("surfaces a configured shell launch error", async () => { | |
| const operations = createLocalBashOperations({ | |
| shellPath: path.join(process.cwd(), "package.json"), | |
| }); | |
| await expect(operations.exec("echo ok", process.cwd(), { onData: () => {} })).rejects.toThrow( | |
| /EACCES|permission denied/i, | |
| ); | |
| }); | |
| it("ignores output callbacks after execution settles", async () => { | |
| const operations: BashOperations = { | |
| exec: async (_command, _cwd, { onData }) => { | |
| onData(Buffer.from("before\n")); | |
| setTimeout(() => onData(Buffer.from("late\n")), 0); | |
| return { exitCode: 0 }; | |
| }, | |
| }; | |
| const tool = createBashTool(process.cwd(), { operations }); | |
| const result = await tool.execute("call-late-output", { command: "ignored" }); | |
| await new Promise<void>((resolve) => { | |
| setTimeout(resolve, 20); | |
| }); | |
| expect(result.content[0]).toEqual({ type: "text", text: "before\n" }); | |
| }); | |
| it.runIf(process.platform !== "win32")( | |
| "tags stdout and stderr from the local shell backend", | |
| async () => { | |
| const operations = createLocalBashOperations(); | |
| const chunks: Array<{ data: Buffer; stream?: "stdout" | "stderr" }> = []; | |
| const result = await operations.exec("printf stdout; printf stderr >&2", process.cwd(), { | |
| onData: (data, stream) => chunks.push({ data, stream }), | |
| }); | |
| expect(result.exitCode).toBe(0); | |
| expect( | |
| Buffer.concat( | |
| chunks.filter((chunk) => chunk.stream === "stdout").map((chunk) => chunk.data), | |
| ).toString("utf8"), | |
| ).toBe("stdout"); | |
| expect( | |
| Buffer.concat( | |
| chunks.filter((chunk) => chunk.stream === "stderr").map((chunk) => chunk.data), | |
| ).toString("utf8"), | |
| ).toBe("stderr"); | |
| expect(chunks.every((chunk) => chunk.stream !== undefined)).toBe(true); | |
| }, | |
| ); | |
| it("decodes a split multi-byte character when the other stream interleaves", async () => { | |
| // stdout and stderr are independent pipes: each needs its own decoder, or a | |
| // character straddling a stdout read boundary is corrupted by an stderr write | |
| // landing between its bytes. | |
| const operations: BashOperations = { | |
| exec: async (_command, _cwd, { onData }) => { | |
| onData(Buffer.from([0xe6, 0x97]), "stdout"); // leading bytes of 日 | |
| onData(Buffer.from("E"), "stderr"); // interleaves mid-character | |
| onData(Buffer.from([0xa5, 0x0a]), "stdout"); | |
| return { exitCode: 0 }; | |
| }, | |
| }; | |
| const tool = createBashTool(process.cwd(), { operations }); | |
| const result = await tool.execute("call-split-utf8", { command: "ignored" }); | |
| expect(result.content[0]).toEqual({ type: "text", text: "E日\n" }); | |
| }); | |
| }); | |