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/workspace/local.test.ts4 * Description: Unit tests for LocalWorkspace — reads, atomic writes, binary/size guards, exec with group kill.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, chmodSync } from "node:fs";11import { tmpdir } from "node:os";12import * as path from "node:path";13import { afterEach, beforeEach, describe, expect, it } from "vitest";14import { LocalWorkspace, isWorkspaceError } from "../../src/workspace/index.js";1516let dir: string;17let ws: LocalWorkspace;1819beforeEach(() => {20 dir = mkdtempSync(path.join(tmpdir(), "khaelor-ws-"));21 ws = new LocalWorkspace(dir);22});2324afterEach(() => {25 rmSync(dir, { recursive: true, force: true });26});2728async function errorCode(promise: Promise<unknown>): Promise<string> {29 try {30 await promise;31 } catch (cause) {32 if (isWorkspaceError(cause)) return cause.code;33 throw cause;34 }35 throw new Error("expected the promise to reject");36}3738describe("LocalWorkspace files", () => {39 it("reads a file via a relative path resolved against cwd", async () => {40 writeFileSync(path.join(dir, "a.txt"), "hello\n");41 expect(ws.cwd()).toBe(dir);42 expect(await ws.readFile("a.txt")).toBe("hello\n");43 });4445 it("reports missing files and directories with typed errors", async () => {46 expect(await errorCode(ws.readFile("nope.txt"))).toBe("file-not-found");47 expect(await errorCode(ws.readFile("."))).toBe("file-is-directory");48 });4950 it("detects binary files via null-byte sniff", async () => {51 writeFileSync(path.join(dir, "bin.dat"), Buffer.from([0x89, 0x50, 0x00, 0x47, 0x0d]));52 expect(await errorCode(ws.readFile("bin.dat"))).toBe("file-binary");53 });5455 it("refuses files larger than the size guard", async () => {56 const small = new LocalWorkspace(dir, { maxReadBytes: 16 });57 writeFileSync(path.join(dir, "big.txt"), "x".repeat(64));58 expect(await errorCode(small.readFile("big.txt"))).toBe("file-too-large");59 });6061 it("writes atomically: correct content, no temp files left behind", async () => {62 await ws.writeFile("out.txt", "first version\n");63 await ws.writeFile("out.txt", "second version\n");64 expect(readFileSync(path.join(dir, "out.txt"), "utf8")).toBe("second version\n");65 const leftovers = readdirSync(dir).filter((name) => name.includes("khaelor-tmp"));66 expect(leftovers).toEqual([]);67 });6869 it("preserves the mode bits of an existing file across overwrite", async () => {70 const file = path.join(dir, "script.sh");71 writeFileSync(file, "#!/bin/sh\n");72 chmodSync(file, 0o755);73 await ws.writeFile("script.sh", "#!/bin/sh\necho updated\n");74 expect(statSync(file).mode & 0o777).toBe(0o755);75 expect(readFileSync(file, "utf8")).toBe("#!/bin/sh\necho updated\n");76 });7778 it("creates parent directories on write", async () => {79 await ws.writeFile("deep/nested/file.txt", "content");80 expect(readFileSync(path.join(dir, "deep/nested/file.txt"), "utf8")).toBe("content");81 });82});8384describe("LocalWorkspace exec", () => {85 it("captures stdout and stderr separately with the exit code", async () => {86 const result = await ws.exec({ cmd: "echo out; echo err 1>&2; exit 3", timeoutMs: 5000 });87 expect(result.stdout).toBe("out\n");88 expect(result.stderr).toBe("err\n");89 expect(result.exitCode).toBe(3);90 expect(result.truncated).toBe(false);91 expect(result.durationMs).toBeGreaterThanOrEqual(0);92 });9394 it("honours cwd and extra env", async () => {95 const result = await ws.exec({96 cmd: "pwd; printf '%s\\n' \"$KHAELOR_TEST_VAR\"",97 cwd: dir,98 env: { KHAELOR_TEST_VAR: "wired" },99 timeoutMs: 5000,100 });101 expect(result.stdout).toContain("wired");102 expect(result.exitCode).toBe(0);103 });104105 it("kills the whole process group on timeout", async () => {106 const started = Date.now();107 // The shell prints the pid of a background sleep, then waits on it.108 const result = await ws.exec({109 cmd: "sleep 30 & echo CHILD:$!; wait",110 timeoutMs: 400,111 });112 expect(Date.now() - started).toBeLessThan(5000);113 expect(result.exitCode).toBeNull();114 const match = /CHILD:(\d+)/.exec(result.stdout);115 expect(match).not.toBeNull();116 const childPid = Number((match as RegExpExecArray)[1]);117 // The grandchild sleep must be dead too (group kill), possibly after a beat.118 await new Promise((resolve) => setTimeout(resolve, 200));119 expect(() => process.kill(childPid, 0)).toThrow();120 });121122 it("kills the process group when the abort signal fires", async () => {123 const controller = new AbortController();124 setTimeout(() => controller.abort(), 200);125 const result = await ws.exec({ cmd: "sleep 30", timeoutMs: 30_000, signal: controller.signal });126 expect(result.exitCode).toBeNull();127 });128129 it("caps captured output and reports truncation", async () => {130 const capped = new LocalWorkspace(dir, { maxExecStreamBytes: 32 });131 const result = await capped.exec({ cmd: "printf 'a%.0s' $(seq 1 200)", timeoutMs: 5000 });132 expect(result.truncated).toBe(true);133 expect(result.stdout.length).toBeLessThanOrEqual(32);134 expect(result.exitCode).toBe(0);135 });136});137