mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 10:31:35 +00:00
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const repoRoot = process.cwd();
|
|
const commonScript = join(repoRoot, "scripts/pr-lib/common.sh");
|
|
const worktreeScript = join(repoRoot, "scripts/pr-lib/worktree.sh");
|
|
const reviewScript = join(repoRoot, "scripts/pr-lib/review.sh");
|
|
const describePosix = process.platform === "win32" ? describe.skip : describe;
|
|
|
|
type Fixture = {
|
|
root: string;
|
|
mainSha: string;
|
|
siblingBranch: string;
|
|
siblingSha: string;
|
|
};
|
|
|
|
function git(root: string, ...args: string[]) {
|
|
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function createFixture(): Fixture {
|
|
const root = realpathSync(tempDirs.make("openclaw-pr-worktree-containment-"));
|
|
git(root, "init", "--initial-branch=main");
|
|
git(root, "config", "user.name", "OpenClaw Test");
|
|
git(root, "config", "user.email", "test@openclaw.invalid");
|
|
writeFileSync(join(root, "fixture.txt"), "main\n");
|
|
git(root, "add", "fixture.txt");
|
|
git(root, "commit", "-m", "main fixture");
|
|
const mainSha = git(root, "rev-parse", "HEAD");
|
|
git(root, "remote", "add", "origin", root);
|
|
git(root, "fetch", "origin");
|
|
git(root, "checkout", "-b", "sibling/work");
|
|
writeFileSync(join(root, "fixture.txt"), "sibling\n");
|
|
git(root, "commit", "-am", "sibling fixture");
|
|
return {
|
|
root,
|
|
mainSha,
|
|
siblingBranch: git(root, "branch", "--show-current"),
|
|
siblingSha: git(root, "rev-parse", "HEAD"),
|
|
};
|
|
}
|
|
|
|
function makeStaleWorktreeDir(fixture: Fixture) {
|
|
mkdirSync(join(fixture.root, ".worktrees", "pr-42"), { recursive: true });
|
|
}
|
|
|
|
function runShell(fixture: Fixture, commands: string[]) {
|
|
return spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
[
|
|
"set -euo pipefail",
|
|
'source "$1"',
|
|
'source "$2"',
|
|
'source "$3"',
|
|
'fixture_root="$4"',
|
|
'repo_root() { printf "%s\\n" "$fixture_root"; }',
|
|
"ensure_gh_api_auth() { :; }",
|
|
"mark_pr_operation_side_effects_started() { :; }",
|
|
"set_review_mode() { :; }",
|
|
...commands,
|
|
].join("\n"),
|
|
"pr-worktree-containment",
|
|
commonScript,
|
|
worktreeScript,
|
|
reviewScript,
|
|
fixture.root,
|
|
],
|
|
{ cwd: fixture.root, encoding: "utf8" },
|
|
);
|
|
}
|
|
|
|
function expectCanonicalCheckoutUnchanged(fixture: Fixture) {
|
|
expect(git(fixture.root, "branch", "--show-current")).toBe(fixture.siblingBranch);
|
|
expect(git(fixture.root, "rev-parse", "HEAD")).toBe(fixture.siblingSha);
|
|
}
|
|
|
|
describePosix("scripts/pr worktree containment", () => {
|
|
it("stale .worktrees/pr-<N> directory does not clobber the canonical checkout", () => {
|
|
const fixture = createFixture();
|
|
makeStaleWorktreeDir(fixture);
|
|
|
|
runShell(fixture, ["enter_worktree 42 true"]);
|
|
|
|
expectCanonicalCheckoutUnchanged(fixture);
|
|
});
|
|
|
|
it("review_checkout_main cannot detach the canonical checkout", () => {
|
|
const fixture = createFixture();
|
|
makeStaleWorktreeDir(fixture);
|
|
|
|
const result = runShell(fixture, ["review_checkout_main 42"]);
|
|
|
|
expectCanonicalCheckoutUnchanged(fixture);
|
|
if (result.status !== 0) {
|
|
expect(result.stderr).toContain("scripts/pr refuses to mutate the shared canonical checkout");
|
|
}
|
|
});
|
|
|
|
it("failure midway leaves the canonical checkout untouched", () => {
|
|
const fixture = createFixture();
|
|
const brokenWorktree = join(fixture.root, ".worktrees", "pr-42");
|
|
git(fixture.root, "worktree", "add", brokenWorktree, "-b", "temp/pr-42", "origin/main");
|
|
rmSync(join(brokenWorktree, ".git"));
|
|
|
|
const result = runShell(fixture, [
|
|
"enter_worktree 42 false",
|
|
"git checkout --detach origin/main",
|
|
"exit 1",
|
|
]);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expectCanonicalCheckoutUnchanged(fixture);
|
|
});
|
|
|
|
it("refuses a symlink alias pointing at another PR's worktree", () => {
|
|
const fixture = createFixture();
|
|
const worktrees = join(fixture.root, ".worktrees");
|
|
mkdirSync(worktrees, { recursive: true });
|
|
git(
|
|
fixture.root,
|
|
"worktree",
|
|
"add",
|
|
join(worktrees, "pr-99"),
|
|
"-b",
|
|
"temp/pr-99",
|
|
"origin/main",
|
|
);
|
|
symlinkSync("pr-99", join(worktrees, "pr-42"), "dir");
|
|
|
|
const result = runShell(fixture, ["enter_worktree 42 true"]);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("refuses to mutate the shared canonical checkout");
|
|
expect(git(join(worktrees, "pr-99"), "branch", "--show-current")).toBe("temp/pr-99");
|
|
expectCanonicalCheckoutUnchanged(fixture);
|
|
});
|
|
|
|
it("reuses a properly registered PR worktree", () => {
|
|
const fixture = createFixture();
|
|
const expectedWorktree = join(fixture.root, ".worktrees", "pr-42");
|
|
git(fixture.root, "worktree", "add", expectedWorktree, "-b", "temp/pr-42", "origin/main");
|
|
|
|
const result = runShell(fixture, [
|
|
"enter_worktree 42 false",
|
|
'printf "cwd=%s\\n" "$PWD"',
|
|
'printf "branch=%s\\n" "$(git branch --show-current)"',
|
|
'printf "head=%s\\n" "$(git rev-parse HEAD)"',
|
|
]);
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
expect(result.stdout).toContain(`cwd=${realpathSync(expectedWorktree)}`);
|
|
expect(result.stdout).toContain("branch=temp/pr-42");
|
|
expect(result.stdout).toContain(`head=${fixture.mainSha}`);
|
|
expectCanonicalCheckoutUnchanged(fixture);
|
|
});
|
|
});
|