SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
4.3 KB · 110 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/tools/bash.test.ts4 * Description: Unit tests for the bash tool — output format, exit codes, timeout redirect, truncation.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { existsSync, mkdirSync, rmSync } from "node:fs";11import * as path from "node:path";12import { afterEach, beforeEach, describe, expect, it } from "vitest";13import { createBashTool } from "../../src/tools/index.js";14import type { TestHarness } from "./helpers.js";15import { makeHarness } from "./helpers.js";1617let h: TestHarness;18const bash = createBashTool();1920beforeEach(() => {21  h = makeHarness();22});2324afterEach(async () => {25  await h.processes.stopAll();26  rmSync(h.dir, { recursive: true, force: true });27});2829describe("bash tool", () => {30  it("returns command output with the $ header and exit-code footer", async () => {31    const result = await bash.execute({ command: "echo hello" }, h.ctx);32    expect(result.isError).toBeUndefined();33    expect(result.content).toContain("$ echo hello");34    expect(result.content).toContain("hello");35    expect(result.content).toMatch(/\[exit code 0 · \d+\.\ds · cwd /);36    expect(result.metadata?.exitCode).toBe(0);37  });3839  it("treats non-zero exits as observations, not errors", async () => {40    const result = await bash.execute({ command: "echo boom >&2; exit 3" }, h.ctx);41    expect(result.isError).toBeUndefined();42    expect(result.content).toContain("boom");43    expect(result.content).toContain("[exit code 3 ·");44    expect(result.metadata?.exitCode).toBe(3);45  });4647  it("honors the workdir parameter", async () => {48    const sub = path.join(h.dir, "subdir");49    mkdirSync(sub);50    const result = await bash.execute({ command: "pwd", workdir: "subdir" }, h.ctx);51    expect(result.content).toContain("subdir");52    expect(result.content).toContain(`cwd ${sub}`);53  });5455  it("redirects a still-running command to the process manager at the ceiling", async () => {56    const result = await bash.execute(57      { command: "sleep 5; echo done", timeout_ms: 300 },58      h.ctx,59    );60    expect(result.isError).toBeUndefined();61    expect(result.content).toContain("Command still running after 0s — moved to background as process p1.");62    expect(result.content).toContain("Output so far:");63    expect(result.content).toContain('Use process {"action":"read","id":"p1"} to see new output');64    expect(result.content).toContain('{"action":"stop","id":"p1"} to stop it.');65    expect(result.metadata?.processId).toBe("p1");6667    // The command was NOT killed — it is alive under the process manager.68    const managed = h.processes.list().find((p) => p.id === "p1");69    expect(managed).toBeDefined();70    expect(managed?.status).toBe("running");71    await h.processes.stop("p1");72  });7374  it("truncates long output middle-out with a spill marker", async () => {75    const result = await bash.execute(76      { command: "i=1; while [ $i -le 600 ]; do echo line-$i; i=$((i+1)); done" },77      h.ctx,78    );79    expect(result.isError).toBeUndefined();80    expect(result.content).toContain("line-1");81    expect(result.content).toContain("line-600");82    expect(result.content).toMatch(/\[\.\.\. [\d,]+ lines omitted \(\d+ KB\)\. Full output: .* — read or grep that file for the rest\.\]/);83    const info = result.metadata?.truncation;84    expect(info).toBeDefined();85    expect(info!.shownHeadLines).toBeLessThanOrEqual(250);86    expect(info!.spillPath).toBeDefined();87    expect(existsSync(info!.spillPath!)).toBe(true);88  });8990  it("interleaves stdout and stderr", async () => {91    const result = await bash.execute({ command: "echo out; echo err >&2" }, h.ctx);92    expect(result.content).toContain("out");93    expect(result.content).toContain("err");94  });9596  it("cancels via the abort signal with the synthetic cancelled result", async () => {97    const promise = bash.execute({ command: "sleep 10", timeout_ms: 30_000 }, h.ctx);98    setTimeout(() => h.abort.abort(), 100);99    const result = await promise;100    expect(result.isError).toBe(true);101    expect(result.content).toBe("[Tool execution cancelled by user]");102  });103104  it("clamps timeout_ms to the 300s ceiling without failing", async () => {105    const result = await bash.execute({ command: "echo fast", timeout_ms: 999_999 }, h.ctx);106    expect(result.content).toContain("fast");107    expect(result.metadata?.exitCode).toBe(0);108  });109});110