/** * KHAELOR * File: tests/tools/process.test.ts * Description: Unit tests for the process tool — start/list/read/write/stop through the tool interface. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { rmSync } from "node:fs"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createProcessTool } from "../../src/tools/index.js"; import type { TestHarness } from "./helpers.js"; import { makeHarness } from "./helpers.js"; let h: TestHarness; const proc = createProcessTool(); beforeEach(() => { h = makeHarness(); }); afterEach(async () => { await h.processes.stopAll(); rmSync(h.dir, { recursive: true, force: true }); }); describe("process tool", () => { it("starts a long-running process and reports pid, cwd, log, and first output", async () => { const result = await proc.execute( { action: "start", command: "echo booted; sleep 30" }, h.ctx, ); expect(result.isError).toBeUndefined(); expect(result.content).toMatch(/^Started p1 \(pid \d+\): echo booted; sleep 30\n/); expect(result.content).toContain(`cwd ${h.dir} · log `); expect(result.content).toContain("First output (waited up to 2s):"); expect(result.content).toContain(" booted"); expect(result.content).toContain('Use process {"action":"read","id":"p1"} for new output.'); expect(result.metadata?.processId).toBe("p1"); }); it("reports an immediate exit inline", async () => { const result = await proc.execute( { action: "start", command: "echo nope; exit 7" }, h.ctx, ); expect(result.isError).toBeUndefined(); expect(result.content).toContain("Process p1 exited immediately with code 7: echo nope; exit 7"); expect(result.content).toContain(" nope"); expect(result.metadata?.exitCode).toBe(7); }); it("lists processes with status columns", async () => { await proc.execute({ action: "start", command: "sleep 30" }, h.ctx); await proc.execute({ action: "start", command: "true" }, h.ctx); const result = await proc.execute({ action: "list" }, h.ctx); expect(result.content).toMatch(/^PROCESSES\n/); expect(result.content).toMatch(/p1 {2}sleep 30\s+running\s+\d{2}:\d{2} {2}pid \d+/); expect(result.content).toMatch(/p2 {2}true\s+exited\s+code 0/); }); it("shows the empty-list guidance", async () => { const result = await proc.execute({ action: "list" }, h.ctx); expect(result.content).toBe( 'No background processes. Use {"action":"start","command":"..."} to launch one.', ); }); it("reads only new output since the last read, then reports none", async () => { await proc.execute( { action: "start", command: "echo one; echo two; sleep 30" }, h.ctx, ); // start consumed the initial lines via its cursor read; produce nothing new. const empty = await proc.execute({ action: "read", id: "p1" }, h.ctx); expect(empty.content).toContain("No new output from p1 since last read (still running, 2 lines total)."); expect(empty.content).toContain("Use offset to re-read earlier output."); const replay = await proc.execute({ action: "read", id: "p1", offset: 1 }, h.ctx); expect(replay.content).toContain("Output of p1 since last read (lines 1–2 of 2):"); expect(replay.content).toContain("one"); expect(replay.content).toContain("two"); }); it("writes to stdin and echoes the response", async () => { await proc.execute({ action: "start", command: "cat" }, h.ctx); const result = await proc.execute({ action: "write", id: "p1", input: "ping\n" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toContain("Sent 5 bytes to p1 stdin."); expect(result.content).toContain("Output:"); expect(result.content).toContain("ping"); }); it("stops a process, reporting exit code and runtime", async () => { await proc.execute({ action: "start", command: "sleep 30" }, h.ctx); const result = await proc.execute({ action: "stop", id: "p1" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toMatch( /^Stopped p1 \(sleep 30\) · exit code null \(SIGTERM\) · ran \d{2}:\d{2}\. Full log: /, ); expect(h.processes.list().find((p) => p.id === "p1")?.status).toBe("stopped"); }); it("explains unknown ids and points at list", async () => { await proc.execute({ action: "start", command: "sleep 30" }, h.ctx); const result = await proc.execute({ action: "read", id: "p9" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toBe( 'No process "p9". Active: p1 (sleep 30, running). Use {"action":"list"} to see all.', ); }); it("produces repair prose for missing conditional parameters", async () => { const noCommand = await proc.execute({ action: "start" }, h.ctx); expect(noCommand.isError).toBe(true); expect(noCommand.content).toBe( 'Invalid input for tool "process": parameter "command" is required for action "start". Please rewrite the input so it satisfies the expected schema.', ); const noId = await proc.execute({ action: "stop" }, h.ctx); expect(noId.isError).toBe(true); expect(noId.content).toContain('parameter "id" is required for action "stop"'); }); it("write on an exited process states the exit and log path", async () => { await proc.execute({ action: "start", command: "true" }, h.ctx); const result = await proc.execute({ action: "write", id: "p1", input: "x\n" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toContain("Process p1 is not running"); expect(result.content).toContain("Full log:"); }); });