Add structured agent helper and checkout git fixes

This commit is contained in:
Mohamed Boudra
2026-01-22 12:23:01 +07:00
parent 88490a9af1
commit cc2bdbf01f
8 changed files with 1016 additions and 50 deletions
@@ -0,0 +1,167 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { execSync } from "child_process";
import { mkdtempSync, rmSync, writeFileSync, realpathSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import {
commitAll,
getCheckoutDiff,
getCheckoutStatus,
mergeToBase,
MergeConflictError,
NotGitRepoError,
} from "./checkout-git.js";
import { createWorktree } from "./worktree.js";
function initRepo(): { tempDir: string; repoDir: string } {
const tempDir = realpathSync(mkdtempSync(join(tmpdir(), "checkout-git-test-")));
const repoDir = join(tempDir, "repo");
execSync(`mkdir -p ${repoDir}`);
execSync("git init -b main", { cwd: repoDir });
execSync("git config user.email 'test@test.com'", { cwd: repoDir });
execSync("git config user.name 'Test'", { cwd: repoDir });
writeFileSync(join(repoDir, "file.txt"), "hello\n");
execSync("git add .", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'initial'", { cwd: repoDir });
return { tempDir, repoDir };
}
describe("checkout git utilities", () => {
let tempDir: string;
let repoDir: string;
beforeEach(() => {
const setup = initRepo();
tempDir = setup.tempDir;
repoDir = setup.repoDir;
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
it("throws NotGitRepoError for non-git directories", async () => {
const nonGitDir = join(tempDir, "not-git");
execSync(`mkdir -p ${nonGitDir}`);
await expect(
getCheckoutDiff(nonGitDir, { mode: "uncommitted" })
).rejects.toBeInstanceOf(NotGitRepoError);
});
it("handles status/diff/commit in a normal repo", async () => {
writeFileSync(join(repoDir, "file.txt"), "updated\n");
const status = await getCheckoutStatus(repoDir);
expect(status.isGit).toBe(true);
expect(status.currentBranch).toBe("main");
expect(status.isDirty).toBe(true);
const diff = await getCheckoutDiff(repoDir, { mode: "uncommitted" });
expect(diff.diff).toContain("-hello");
expect(diff.diff).toContain("+updated");
await commitAll(repoDir, "update file");
const cleanStatus = await getCheckoutStatus(repoDir);
expect(cleanStatus.isDirty).toBe(false);
const message = execSync("git log -1 --pretty=%B", { cwd: repoDir })
.toString()
.trim();
expect(message).toBe("update file");
});
it("commits messages with quotes safely", async () => {
const message = `He said "hello" and it's fine`;
writeFileSync(join(repoDir, "file.txt"), "quoted\n");
await commitAll(repoDir, message);
const logMessage = execSync("git log -1 --pretty=%B", { cwd: repoDir })
.toString()
.trim();
expect(logMessage).toBe(message);
});
it("handles status/diff/commit in a .paseo worktree", async () => {
const result = await createWorktree({
branchName: "main",
cwd: repoDir,
worktreeSlug: "alpha",
});
writeFileSync(join(result.worktreePath, "file.txt"), "worktree change\n");
const status = await getCheckoutStatus(result.worktreePath);
expect(status.isGit).toBe(true);
expect(status.repoRoot).toBe(repoDir);
expect(status.isDirty).toBe(true);
const diff = await getCheckoutDiff(result.worktreePath, { mode: "uncommitted" });
expect(diff.diff).toContain("-hello");
expect(diff.diff).toContain("+worktree change");
await commitAll(result.worktreePath, "worktree update");
const cleanStatus = await getCheckoutStatus(result.worktreePath);
expect(cleanStatus.isDirty).toBe(false);
const message = execSync("git log -1 --pretty=%B", {
cwd: result.worktreePath,
})
.toString()
.trim();
expect(message).toBe("worktree update");
});
it("merges the current branch into base", async () => {
writeFileSync(join(repoDir, "merge.txt"), "feature\n");
execSync("git checkout -b feature", { cwd: repoDir });
execSync("git add merge.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'feature commit'", { cwd: repoDir });
const featureCommit = execSync("git rev-parse HEAD", { cwd: repoDir })
.toString()
.trim();
await mergeToBase(repoDir, { baseRef: "main" });
const baseContainsFeature = execSync(`git merge-base --is-ancestor ${featureCommit} main`, {
cwd: repoDir,
stdio: "pipe",
});
expect(baseContainsFeature).toBeDefined();
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: repoDir })
.toString()
.trim();
expect(currentBranch).toBe("feature");
});
it("returns typed MergeConflictError on merge conflicts", async () => {
const conflictFile = join(repoDir, "conflict.txt");
writeFileSync(conflictFile, "base\n");
execSync("git add conflict.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'add conflict file'", {
cwd: repoDir,
});
execSync("git checkout -b feature", { cwd: repoDir });
writeFileSync(conflictFile, "feature change\n");
execSync("git add conflict.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'feature change'", {
cwd: repoDir,
});
execSync("git checkout main", { cwd: repoDir });
writeFileSync(conflictFile, "main change\n");
execSync("git add conflict.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'main change'", {
cwd: repoDir,
});
execSync("git checkout feature", { cwd: repoDir });
await expect(
mergeToBase(repoDir, { baseRef: "main" })
).rejects.toBeInstanceOf(MergeConflictError);
});
});