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%
4.5 KB · 119 lines typescript
Raw Blame History
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