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%
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