131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { mkdtemp, rm, realpath } from "node:fs/promises";
|
|
import { existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import worktreePlugin from "@composio/ao-plugin-workspace-worktree";
|
|
import type { ProjectConfig, WorkspaceInfo } from "@composio/ao-core";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
async function git(cwd: string, ...args: string[]): Promise<string> {
|
|
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
return stdout.trimEnd();
|
|
}
|
|
|
|
describe("workspace-worktree (integration)", () => {
|
|
let repoDir: string;
|
|
let worktreeBaseDir: string;
|
|
let workspace: ReturnType<typeof worktreePlugin.create>;
|
|
let project: ProjectConfig;
|
|
let createdInfo: WorkspaceInfo;
|
|
|
|
beforeAll(async () => {
|
|
// Create a temp repo with initial commit
|
|
const rawRepo = await mkdtemp(join(tmpdir(), "ao-inttest-wt-repo-"));
|
|
repoDir = await realpath(rawRepo);
|
|
|
|
await git(repoDir, "init", "-b", "main");
|
|
await git(repoDir, "config", "user.email", "test@test.com");
|
|
await git(repoDir, "config", "user.name", "Test");
|
|
await execFileAsync("sh", ["-c", "echo hello > README.md"], { cwd: repoDir });
|
|
await git(repoDir, "add", ".");
|
|
await git(repoDir, "commit", "-m", "initial commit");
|
|
|
|
// Add "origin" pointing at itself so the plugin's fetch succeeds
|
|
await git(repoDir, "remote", "add", "origin", repoDir);
|
|
await git(repoDir, "fetch", "origin");
|
|
|
|
// Create worktree base dir
|
|
const rawBase = await mkdtemp(join(tmpdir(), "ao-inttest-wt-base-"));
|
|
worktreeBaseDir = await realpath(rawBase);
|
|
|
|
workspace = worktreePlugin.create({ worktreeDir: worktreeBaseDir });
|
|
|
|
project = {
|
|
name: "inttest",
|
|
repo: "test/inttest",
|
|
path: repoDir,
|
|
defaultBranch: "main",
|
|
sessionPrefix: "test",
|
|
};
|
|
}, 30_000);
|
|
|
|
afterAll(async () => {
|
|
// Clean up worktrees first (must be done before removing repo)
|
|
try {
|
|
await git(repoDir, "worktree", "prune");
|
|
} catch { /* best-effort cleanup */ }
|
|
if (repoDir) await rm(repoDir, { recursive: true, force: true }).catch(() => {});
|
|
if (worktreeBaseDir) await rm(worktreeBaseDir, { recursive: true, force: true }).catch(() => {});
|
|
}, 30_000);
|
|
|
|
it("creates a worktree workspace", async () => {
|
|
createdInfo = await workspace.create({
|
|
projectId: "inttest",
|
|
sessionId: "session-1",
|
|
project,
|
|
branch: "feat/test-branch",
|
|
});
|
|
|
|
expect(createdInfo.path).toContain("session-1");
|
|
expect(createdInfo.branch).toBe("feat/test-branch");
|
|
expect(createdInfo.sessionId).toBe("session-1");
|
|
expect(createdInfo.projectId).toBe("inttest");
|
|
expect(existsSync(createdInfo.path)).toBe(true);
|
|
});
|
|
|
|
it("worktree is on the correct branch", async () => {
|
|
const branch = await git(createdInfo.path, "branch", "--show-current");
|
|
expect(branch).toBe("feat/test-branch");
|
|
});
|
|
|
|
it("worktree has the files from main", async () => {
|
|
expect(existsSync(join(createdInfo.path, "README.md"))).toBe(true);
|
|
});
|
|
|
|
it("lists the worktree", async () => {
|
|
const list = await workspace.list("inttest");
|
|
expect(list.length).toBeGreaterThanOrEqual(1);
|
|
const found = list.find((w: { sessionId: string }) => w.sessionId === "session-1");
|
|
expect(found).toBeDefined();
|
|
expect(found!.branch).toBe("feat/test-branch");
|
|
});
|
|
|
|
it("rejects invalid projectId", async () => {
|
|
await expect(
|
|
workspace.create({
|
|
projectId: "../escape",
|
|
sessionId: "ok",
|
|
project,
|
|
branch: "feat/x",
|
|
}),
|
|
).rejects.toThrow("Invalid projectId");
|
|
});
|
|
|
|
it("rejects invalid sessionId", async () => {
|
|
await expect(
|
|
workspace.create({
|
|
projectId: "inttest",
|
|
sessionId: "bad/id",
|
|
project,
|
|
branch: "feat/x",
|
|
}),
|
|
).rejects.toThrow("Invalid sessionId");
|
|
});
|
|
|
|
it("destroys the worktree", async () => {
|
|
const pathToDestroy = createdInfo.path;
|
|
await workspace.destroy(pathToDestroy);
|
|
expect(existsSync(pathToDestroy)).toBe(false);
|
|
});
|
|
|
|
it("list returns empty after destroy", async () => {
|
|
const list = await workspace.list("inttest");
|
|
const found = list.find((w: { sessionId: string }) => w.sessionId === "session-1");
|
|
expect(found).toBeUndefined();
|
|
});
|
|
});
|