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