/** * KHAELOR * File: tests/tasks/worktree.test.ts * Description: Worktree subtask tests — create/diff/merge/remove against a real temp git repo (v2 §6). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { execFileSync } from "node:child_process"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { createWorktree, mergeSubtaskBranch, removeWorktree, worktreeDiffStats, } from "../../src/tasks/index.js"; import type { ExecFn } from "../../src/tasks/index.js"; import { SubtaskManager } from "../../src/tasks/index.js"; import type { DurableEventInput } from "../../src/session/index.js"; const exec: ExecFn = (cmd, cwd) => { try { const stdout = execFileSync("/bin/sh", ["-c", cmd], { cwd, encoding: "utf8" }); return Promise.resolve({ exitCode: 0, stdout, stderr: "" }); } catch (error) { const failure = error as { status?: number; stdout?: string; stderr?: string }; return Promise.resolve({ exitCode: failure.status ?? 1, stdout: failure.stdout?.toString() ?? "", stderr: failure.stderr?.toString() ?? "", }); } }; function makeRepo(): string { const dir = mkdtempSync(join(tmpdir(), "khaelor-worktree-")); execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir }); execFileSync("git", ["config", "user.email", "t@t"], { cwd: dir }); execFileSync("git", ["config", "user.name", "t"], { cwd: dir }); writeFileSync(join(dir, "a.txt"), "hello\n"); execFileSync("git", ["add", "-A"], { cwd: dir }); execFileSync("git", ["commit", "-qm", "init"], { cwd: dir }); return dir; } describe("worktree lifecycle", () => { it("creates, diffs, merges (--no-ff), and removes a subtask worktree", async () => { const repo = makeRepo(); const worktree = await createWorktree(exec, repo, "t1"); expect(worktree.branch).toBe("khaelor/t1"); writeFileSync(join(worktree.path, "b.txt"), "new file\nsecond line\n"); await exec("git add -A && git commit -qm change", worktree.path); const diff = await worktreeDiffStats(exec, repo, worktree.branch); expect(diff.added).toBe(2); expect(diff.files).toEqual(["b.txt"]); const merge = await mergeSubtaskBranch(exec, repo, worktree.branch, "merge t1"); expect(merge.ok).toBe(true); const log = await exec("git log --oneline -1", repo); expect(log.stdout).toContain("merge t1"); await removeWorktree(exec, repo, worktree, { deleteBranch: true }); const branches = await exec("git branch --list 'khaelor/*'", repo); expect(branches.stdout.trim()).toBe(""); }); it("aborts cleanly on merge conflicts", async () => { const repo = makeRepo(); const worktree = await createWorktree(exec, repo, "t2"); writeFileSync(join(worktree.path, "a.txt"), "worktree version\n"); await exec("git add -A && git commit -qm wt", worktree.path); writeFileSync(join(repo, "a.txt"), "main version\n"); await exec("git add -A && git commit -qm main", repo); const merge = await mergeSubtaskBranch(exec, repo, worktree.branch, "merge t2"); expect(merge.ok).toBe(false); expect(merge.conflict).toBe(true); const status = await exec("git status --porcelain", repo); expect(status.stdout.trim()).toBe(""); // aborted — tree left clean }); }); describe("SubtaskManager", () => { it("spawns a child run in a worktree and records subtask events with real diff stats", async () => { const repo = makeRepo(); const published: DurableEventInput[] = []; const manager = new SubtaskManager({ publish: (event) => published.push(event), exec, projectRoot: repo, runChild: (args) => { writeFileSync(join(args.worktreePath, "feature.txt"), "done by child\n"); return Promise.resolve({ status: "done" as const, verifyOk: true, detail: "implemented" }); }, }); const record = await manager.spawn("add the feature"); expect(record.status).toBe("running"); await manager.waitAll(); const done = manager.get(record.taskId); expect(done?.status).toBe("done"); expect(done?.diff.added).toBe(1); expect(done?.verifyOk).toBe(true); const types = published.map((event) => event.type); expect(types).toEqual(["subtask.created", "subtask.completed"]); const completed = published[1]; expect( completed !== undefined && completed.type === "subtask.completed" ? completed.payload.diffStats.added : 0, ).toBe(1); }); });