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/tasks/worktree.test.ts4 * Description: Worktree subtask tests — create/diff/merge/remove against a real temp git repo (v2 §6).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { execFileSync } from "node:child_process";11import { mkdtempSync, writeFileSync } from "node:fs";12import { tmpdir } from "node:os";13import { join } from "node:path";14import { describe, expect, it } from "vitest";15import {16 createWorktree,17 mergeSubtaskBranch,18 removeWorktree,19 worktreeDiffStats,20} from "../../src/tasks/index.js";21import type { ExecFn } from "../../src/tasks/index.js";22import { SubtaskManager } from "../../src/tasks/index.js";23import type { DurableEventInput } from "../../src/session/index.js";2425const exec: ExecFn = (cmd, cwd) => {26 try {27 const stdout = execFileSync("/bin/sh", ["-c", cmd], { cwd, encoding: "utf8" });28 return Promise.resolve({ exitCode: 0, stdout, stderr: "" });29 } catch (error) {30 const failure = error as { status?: number; stdout?: string; stderr?: string };31 return Promise.resolve({32 exitCode: failure.status ?? 1,33 stdout: failure.stdout?.toString() ?? "",34 stderr: failure.stderr?.toString() ?? "",35 });36 }37};3839function makeRepo(): string {40 const dir = mkdtempSync(join(tmpdir(), "khaelor-worktree-"));41 execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir });42 execFileSync("git", ["config", "user.email", "t@t"], { cwd: dir });43 execFileSync("git", ["config", "user.name", "t"], { cwd: dir });44 writeFileSync(join(dir, "a.txt"), "hello\n");45 execFileSync("git", ["add", "-A"], { cwd: dir });46 execFileSync("git", ["commit", "-qm", "init"], { cwd: dir });47 return dir;48}4950describe("worktree lifecycle", () => {51 it("creates, diffs, merges (--no-ff), and removes a subtask worktree", async () => {52 const repo = makeRepo();53 const worktree = await createWorktree(exec, repo, "t1");54 expect(worktree.branch).toBe("khaelor/t1");5556 writeFileSync(join(worktree.path, "b.txt"), "new file\nsecond line\n");57 await exec("git add -A && git commit -qm change", worktree.path);5859 const diff = await worktreeDiffStats(exec, repo, worktree.branch);60 expect(diff.added).toBe(2);61 expect(diff.files).toEqual(["b.txt"]);6263 const merge = await mergeSubtaskBranch(exec, repo, worktree.branch, "merge t1");64 expect(merge.ok).toBe(true);65 const log = await exec("git log --oneline -1", repo);66 expect(log.stdout).toContain("merge t1");6768 await removeWorktree(exec, repo, worktree, { deleteBranch: true });69 const branches = await exec("git branch --list 'khaelor/*'", repo);70 expect(branches.stdout.trim()).toBe("");71 });7273 it("aborts cleanly on merge conflicts", async () => {74 const repo = makeRepo();75 const worktree = await createWorktree(exec, repo, "t2");76 writeFileSync(join(worktree.path, "a.txt"), "worktree version\n");77 await exec("git add -A && git commit -qm wt", worktree.path);78 writeFileSync(join(repo, "a.txt"), "main version\n");79 await exec("git add -A && git commit -qm main", repo);8081 const merge = await mergeSubtaskBranch(exec, repo, worktree.branch, "merge t2");82 expect(merge.ok).toBe(false);83 expect(merge.conflict).toBe(true);84 const status = await exec("git status --porcelain", repo);85 expect(status.stdout.trim()).toBe(""); // aborted — tree left clean86 });87});8889describe("SubtaskManager", () => {90 it("spawns a child run in a worktree and records subtask events with real diff stats", async () => {91 const repo = makeRepo();92 const published: DurableEventInput[] = [];93 const manager = new SubtaskManager({94 publish: (event) => published.push(event),95 exec,96 projectRoot: repo,97 runChild: (args) => {98 writeFileSync(join(args.worktreePath, "feature.txt"), "done by child\n");99 return Promise.resolve({ status: "done" as const, verifyOk: true, detail: "implemented" });100 },101 });102 const record = await manager.spawn("add the feature");103 expect(record.status).toBe("running");104 await manager.waitAll();105106 const done = manager.get(record.taskId);107 expect(done?.status).toBe("done");108 expect(done?.diff.added).toBe(1);109 expect(done?.verifyOk).toBe(true);110111 const types = published.map((event) => event.type);112 expect(types).toEqual(["subtask.created", "subtask.completed"]);113 const completed = published[1];114 expect(115 completed !== undefined && completed.type === "subtask.completed" ? completed.payload.diffStats.added : 0,116 ).toBe(1);117 });118});119