Files
oh-my-pi/packages/coding-agent/test/autoresearch-git.test.ts
T
roboomp 7ba2227c3c fix(coding-agent): guarded Git-mutating automation in pure jj workspaces
Worktree setup and autoresearch Git prep silently misbehaved in pure Jujutsu workspaces (`.jj/repo/` present, no colocated `.git/`):

- `task/worktree.ts#getRepoRoot` threw a generic "Git repository not found for isolated task execution.", giving a jj user no hint about what was wrong.

- `autoresearch/git.ts#ensureAutoresearchBranch` returned a soft "Not in a git repository" warning, letting `/autoresearch` proceed with no branch isolation, baseline reset, or auto-commits.

Both paths now detect a pure jj workspace via a new `jj.isPureJjRepo` helper and surface an actionable Jujutsu-specific error pointing at `jj git init --colocate`. Colocated jj-git workspaces (both `.jj/` and `.git/` at the same root) and plain Git checkouts behave exactly as before.

Fixes #1935
2026-06-05 14:39:39 +00:00

96 lines
3.6 KiB
TypeScript

import { afterEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { ensureAutoresearchBranch } from "../src/autoresearch/git";
import type { ExtensionAPI } from "../src/extensibility/extensions";
import * as jj from "../src/utils/jj";
const tempDirs: string[] = [];
async function mkTempDir(prefix: string): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
async function runGit(cwd: string, args: string[]): Promise<void> {
const env = { ...process.env, HOME: cwd, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" };
const proc = Bun.spawn(["git", "-C", cwd, ...args], { env, stdout: "ignore", stderr: "pipe" });
const code = await proc.exited;
if (code !== 0) {
const stderr = await new Response(proc.stderr).text();
throw new Error(`git ${args.join(" ")} failed (${code}): ${stderr}`);
}
}
async function initGitWithCommit(dir: string): Promise<void> {
await runGit(dir, ["init", "-q", "-b", "main"]);
await runGit(dir, ["config", "user.email", "test@example.com"]);
await runGit(dir, ["config", "user.name", "Test"]);
await fs.writeFile(path.join(dir, "README"), "seed\n");
await runGit(dir, ["add", "."]);
await runGit(dir, ["commit", "-q", "-m", "init"]);
}
// `ensureAutoresearchBranch` never invokes the `api` it receives — its two
// internal helpers (`readGitWorkDirPrefix`, `branchExists`) immediately
// `void api`. The stub keeps the signature honest without spinning up the
// real extension runtime.
const stubApi = {} as unknown as ExtensionAPI;
afterEach(async () => {
jj.repo.clearRootCache();
await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })));
});
describe("ensureAutoresearchBranch jj guardrails", () => {
it("rejects pure jj workspaces before touching git state", async () => {
const dir = await mkTempDir("omp-ar-purejj-");
await fs.mkdir(path.join(dir, ".jj", "repo", "store"), { recursive: true });
const result = await ensureAutoresearchBranch(stubApi, dir, "demo");
expect(result.ok).toBe(false);
if (result.ok) throw new Error("unreachable");
expect(result.error).toMatch(/pure Jujutsu/);
expect(result.error).toMatch(/jj git init --colocate/);
});
it("creates an autoresearch branch in a colocated jj-git workspace", async () => {
const dir = await mkTempDir("omp-ar-colocated-");
await initGitWithCommit(dir);
await fs.mkdir(path.join(dir, ".jj", "repo", "store"), { recursive: true });
const result = await ensureAutoresearchBranch(stubApi, dir, "demo goal");
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("unreachable");
expect(result.created).toBe(true);
expect(result.branchName).toMatch(/^autoresearch\/demo-goal-\d{8}$/);
});
it("creates an autoresearch branch in a plain git repo (unchanged behavior)", async () => {
const dir = await mkTempDir("omp-ar-plaingit-");
await initGitWithCommit(dir);
const result = await ensureAutoresearchBranch(stubApi, dir, "demo");
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("unreachable");
expect(result.created).toBe(true);
expect(result.branchName).toMatch(/^autoresearch\/demo-\d{8}$/);
});
it("returns the soft no-git warning for directories backed by neither tool", async () => {
const dir = await mkTempDir("omp-ar-empty-");
const result = await ensureAutoresearchBranch(stubApi, dir, "demo");
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("unreachable");
expect(result.branchName).toBeNull();
expect(result.warning).toMatch(/Not in a git repository/);
});
});