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%
5.6 KB · 133 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/tools/process.test.ts4 * Description: Unit tests for the process tool — start/list/read/write/stop through the tool interface.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { rmSync } from "node:fs";11import { afterEach, beforeEach, describe, expect, it } from "vitest";12import { createProcessTool } from "../../src/tools/index.js";13import type { TestHarness } from "./helpers.js";14import { makeHarness } from "./helpers.js";1516let h: TestHarness;17const proc = createProcessTool();1819beforeEach(() => {20  h = makeHarness();21});2223afterEach(async () => {24  await h.processes.stopAll();25  rmSync(h.dir, { recursive: true, force: true });26});2728describe("process tool", () => {29  it("starts a long-running process and reports pid, cwd, log, and first output", async () => {30    const result = await proc.execute(31      { action: "start", command: "echo booted; sleep 30" },32      h.ctx,33    );34    expect(result.isError).toBeUndefined();35    expect(result.content).toMatch(/^Started p1 \(pid \d+\): echo booted; sleep 30\n/);36    expect(result.content).toContain(`cwd ${h.dir} · log `);37    expect(result.content).toContain("First output (waited up to 2s):");38    expect(result.content).toContain("  booted");39    expect(result.content).toContain('Use process {"action":"read","id":"p1"} for new output.');40    expect(result.metadata?.processId).toBe("p1");41  });4243  it("reports an immediate exit inline", async () => {44    const result = await proc.execute(45      { action: "start", command: "echo nope; exit 7" },46      h.ctx,47    );48    expect(result.isError).toBeUndefined();49    expect(result.content).toContain("Process p1 exited immediately with code 7: echo nope; exit 7");50    expect(result.content).toContain("  nope");51    expect(result.metadata?.exitCode).toBe(7);52  });5354  it("lists processes with status columns", async () => {55    await proc.execute({ action: "start", command: "sleep 30" }, h.ctx);56    await proc.execute({ action: "start", command: "true" }, h.ctx);57    const result = await proc.execute({ action: "list" }, h.ctx);58    expect(result.content).toMatch(/^PROCESSES\n/);59    expect(result.content).toMatch(/p1 {2}sleep 30\s+running\s+\d{2}:\d{2} {2}pid \d+/);60    expect(result.content).toMatch(/p2 {2}true\s+exited\s+code 0/);61  });6263  it("shows the empty-list guidance", async () => {64    const result = await proc.execute({ action: "list" }, h.ctx);65    expect(result.content).toBe(66      'No background processes. Use {"action":"start","command":"..."} to launch one.',67    );68  });6970  it("reads only new output since the last read, then reports none", async () => {71    await proc.execute(72      { action: "start", command: "echo one; echo two; sleep 30" },73      h.ctx,74    );75    // start consumed the initial lines via its cursor read; produce nothing new.76    const empty = await proc.execute({ action: "read", id: "p1" }, h.ctx);77    expect(empty.content).toContain("No new output from p1 since last read (still running, 2 lines total).");78    expect(empty.content).toContain("Use offset to re-read earlier output.");7980    const replay = await proc.execute({ action: "read", id: "p1", offset: 1 }, h.ctx);81    expect(replay.content).toContain("Output of p1 since last read (lines 1–2 of 2):");82    expect(replay.content).toContain("one");83    expect(replay.content).toContain("two");84  });8586  it("writes to stdin and echoes the response", async () => {87    await proc.execute({ action: "start", command: "cat" }, h.ctx);88    const result = await proc.execute({ action: "write", id: "p1", input: "ping\n" }, h.ctx);89    expect(result.isError).toBeUndefined();90    expect(result.content).toContain("Sent 5 bytes to p1 stdin.");91    expect(result.content).toContain("Output:");92    expect(result.content).toContain("ping");93  });9495  it("stops a process, reporting exit code and runtime", async () => {96    await proc.execute({ action: "start", command: "sleep 30" }, h.ctx);97    const result = await proc.execute({ action: "stop", id: "p1" }, h.ctx);98    expect(result.isError).toBeUndefined();99    expect(result.content).toMatch(100      /^Stopped p1 \(sleep 30\) · exit code null \(SIGTERM\) · ran \d{2}:\d{2}\. Full log: /,101    );102    expect(h.processes.list().find((p) => p.id === "p1")?.status).toBe("stopped");103  });104105  it("explains unknown ids and points at list", async () => {106    await proc.execute({ action: "start", command: "sleep 30" }, h.ctx);107    const result = await proc.execute({ action: "read", id: "p9" }, h.ctx);108    expect(result.isError).toBe(true);109    expect(result.content).toBe(110      'No process "p9". Active: p1 (sleep 30, running). Use {"action":"list"} to see all.',111    );112  });113114  it("produces repair prose for missing conditional parameters", async () => {115    const noCommand = await proc.execute({ action: "start" }, h.ctx);116    expect(noCommand.isError).toBe(true);117    expect(noCommand.content).toBe(118      'Invalid input for tool "process": parameter "command" is required for action "start". Please rewrite the input so it satisfies the expected schema.',119    );120    const noId = await proc.execute({ action: "stop" }, h.ctx);121    expect(noId.isError).toBe(true);122    expect(noId.content).toContain('parameter "id" is required for action "stop"');123  });124125  it("write on an exited process states the exit and log path", async () => {126    await proc.execute({ action: "start", command: "true" }, h.ctx);127    const result = await proc.execute({ action: "write", id: "p1", input: "x\n" }, h.ctx);128    expect(result.isError).toBe(true);129    expect(result.content).toContain("Process p1 is not running");130    expect(result.content).toContain("Full log:");131  });132});133