test(agents): reuse managed worktree fixtures (#118619)

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
This commit is contained in:
Peter Steinberger
2026-08-03 02:59:47 -07:00
committed by GitHub
parent a7396255d0
commit cc940b5ff5
6 changed files with 275 additions and 164 deletions
+1
View File
@@ -77,6 +77,7 @@ describe("worktree run lease", () => {
const created = await service.create({
repoRoot: repo,
name: "run-lease-session",
baseRef: "HEAD",
ownerKind: "session",
ownerId: "agent:main:run-lease",
});
+19 -10
View File
@@ -42,9 +42,13 @@ describe("ManagedWorktreeService naming", () => {
});
it("uses readable defaults and numbers colliding inferred names", async () => {
const fallback = await service.create({ repoRoot: repo });
await service.create({ repoRoot: repo, name: "release-planning" });
const second = await service.create({ repoRoot: repo, suggestedName: "release-planning" });
const fallback = await service.create({ repoRoot: repo, baseRef: "HEAD" });
await service.create({ repoRoot: repo, name: "release-planning", baseRef: "HEAD" });
const second = await service.create({
repoRoot: repo,
suggestedName: "release-planning",
baseRef: "HEAD",
});
expect(fallback.name).toMatch(
/^[a-z]+-(?:barnacle|claw|crab|crayfish|krill|langoustine|lobster|prawn|shrimp|shell)$/,
@@ -53,19 +57,23 @@ describe("ManagedWorktreeService naming", () => {
});
it("numbers inferred names around unmanaged Git and filesystem collisions", async () => {
const anchor = await service.create({ repoRoot: repo, name: "anchor" });
const anchor = await service.create({ repoRoot: repo, name: "anchor", baseRef: "HEAD" });
await git(repo, "branch", "openclaw/release-planning");
await fs.mkdir(path.join(path.dirname(anchor.path), "release-planning-2"));
const created = await service.create({ repoRoot: repo, suggestedName: "release-planning" });
const created = await service.create({
repoRoot: repo,
suggestedName: "release-planning",
baseRef: "HEAD",
});
expect(created.name).toBe("release-planning-3");
});
it("serializes concurrent inferred-name creation", async () => {
const created = await Promise.all([
service.create({ repoRoot: repo, suggestedName: "concurrent-task" }),
service.create({ repoRoot: repo, suggestedName: "concurrent-task" }),
service.create({ repoRoot: repo, suggestedName: "concurrent-task", baseRef: "HEAD" }),
service.create({ repoRoot: repo, suggestedName: "concurrent-task", baseRef: "HEAD" }),
]);
expect(created.map((record) => record.name).toSorted()).toEqual([
@@ -77,6 +85,7 @@ describe("ManagedWorktreeService naming", () => {
it("reuses concurrent inferred names for the same owner", async () => {
const owner = {
repoRoot: repo,
baseRef: "HEAD",
ownerKind: "session" as const,
ownerId: "agent:main:session-1",
};
@@ -93,11 +102,11 @@ describe("ManagedWorktreeService naming", () => {
});
it("serializes overlapping numeric suffix families", async () => {
await service.create({ repoRoot: repo, name: "task" });
await service.create({ repoRoot: repo, name: "task", baseRef: "HEAD" });
const created = await Promise.all([
service.create({ repoRoot: repo, suggestedName: "task" }),
service.create({ repoRoot: repo, suggestedName: "task-2" }),
service.create({ repoRoot: repo, suggestedName: "task", baseRef: "HEAD" }),
service.create({ repoRoot: repo, suggestedName: "task-2", baseRef: "HEAD" }),
]);
const names = created.map((record) => record.name);
@@ -83,7 +83,7 @@ describe("ManagedWorktreeService provisioned state", () => {
await fs.writeFile(path.join(repo, "large.local"), source);
await addRemote(root, repo);
const created = await service.create({ repoRoot: repo, name: "large-local" });
const created = await service.create({ repoRoot: repo, name: "large-local", baseRef: "HEAD" });
await service.acquire(created.id);
const copyPath = path.join(created.path, "large.local");
const copy = Buffer.from(source);
@@ -105,9 +105,21 @@ describe("ManagedWorktreeService provisioned state", () => {
await fs.writeFile(path.join(repo, "settings.local"), "theme=source\n");
await addRemote(root, repo);
const manifestRemoved = await service.create({ repoRoot: repo, name: "manifest-removed" });
const patternRemoved = await service.create({ repoRoot: repo, name: "pattern-removed" });
const restorable = await service.create({ repoRoot: repo, name: "manifest-restorable" });
const manifestRemoved = await service.create({
repoRoot: repo,
name: "manifest-removed",
baseRef: "HEAD",
});
const patternRemoved = await service.create({
repoRoot: repo,
name: "pattern-removed",
baseRef: "HEAD",
});
const restorable = await service.create({
repoRoot: repo,
name: "manifest-restorable",
baseRef: "HEAD",
});
await service.acquire(manifestRemoved.id);
await service.acquire(patternRemoved.id);
await service.acquire(restorable.id);
@@ -177,7 +189,11 @@ describe("ManagedWorktreeService provisioned state", () => {
await git(repo, "commit", "-m", "configure worktree provisioning");
await fs.writeFile(path.join(repo, ".env.local"), "value=source\n");
const tracked = await service.create({ repoRoot: repo, name: "tracked-provisioned" });
const tracked = await service.create({
repoRoot: repo,
name: "tracked-provisioned",
baseRef: "HEAD",
});
await git(tracked.path, "add", "-f", ".env.local");
await git(tracked.path, "commit", "-m", "track provisioned file");
await expect(service.remove({ id: tracked.id, reason: "manual" })).rejects.toThrow(
@@ -188,7 +204,11 @@ describe("ManagedWorktreeService provisioned state", () => {
"provisioned path is tracked at HEAD",
);
const unignored = await service.create({ repoRoot: repo, name: "unignored-provisioned" });
const unignored = await service.create({
repoRoot: repo,
name: "unignored-provisioned",
baseRef: "HEAD",
});
await fs.writeFile(path.join(unignored.path, ".gitignore"), "");
await expect(service.remove({ id: unignored.id, reason: "manual" })).rejects.toThrow(
"provisioned path is no longer ignored",
@@ -212,7 +232,11 @@ describe("ManagedWorktreeService provisioned state", () => {
await fs.writeFile(path.join(repo, wildcardName), "wildcard source\n");
await fs.writeFile(path.join(repo, backslashName), "backslash source\n");
const created = await service.create({ repoRoot: repo, name: "literal-paths" });
const created = await service.create({
repoRoot: repo,
name: "literal-paths",
baseRef: "HEAD",
});
await fs.writeFile(path.join(created.path, wildcardName), "wildcard local\n");
await fs.writeFile(path.join(created.path, backslashName), "backslash local\n");
await service.remove({ id: created.id, reason: "test" });
@@ -228,7 +252,11 @@ describe("ManagedWorktreeService provisioned state", () => {
);
it("snapshots deleted skip-worktree files still included by sparse rules", async () => {
const created = await service.create({ repoRoot: repo, name: "stale-sparse-bit" });
const created = await service.create({
repoRoot: repo,
name: "stale-sparse-bit",
baseRef: "HEAD",
});
await git(created.path, "sparse-checkout", "set", "--no-cone", "/*");
await git(created.path, "update-index", "--skip-worktree", "README.md");
await fs.rm(path.join(created.path, "README.md"));
@@ -250,7 +278,7 @@ describe("ManagedWorktreeService provisioned state", () => {
await git(repo, "add", "-A");
await git(repo, "commit", "-m", "add raw path");
const created = await service.create({ repoRoot: repo, name: "raw-path" });
const created = await service.create({ repoRoot: repo, name: "raw-path", baseRef: "HEAD" });
const worktreePath = Buffer.concat([
Buffer.from(created.path),
Buffer.from(path.sep),
@@ -55,6 +55,7 @@ describe("ManagedWorktreeService removal against a live run lease", () => {
const created = await service.create({
repoRoot: repo,
name: "removal-session",
baseRef: "HEAD",
ownerKind: "session",
ownerId: "agent:main:removal",
});
@@ -0,0 +1,68 @@
import { execFile } from "node:child_process";
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { insertRegistryWorktree } from "./registry.js";
import type { ManagedWorktreeOwnerKind, ManagedWorktreeRecord } from "./types.js";
const execFileAsync = promisify(execFile);
async function git(cwd: string, ...args: string[]): Promise<void> {
await execFileAsync("git", ["-C", cwd, ...args]);
}
async function copyProvisionedFiles(params: {
repoRoot: string;
worktreePath: string;
provisionedPaths: readonly string[];
}): Promise<void> {
for (const provisionedPath of params.provisionedPaths) {
const source = path.join(params.repoRoot, provisionedPath);
const target = path.join(params.worktreePath, provisionedPath);
const sourceStat = await fs.lstat(source);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.copyFile(source, target, fsConstants.COPYFILE_FICLONE);
if (process.platform !== "win32") {
await fs.chmod(target, sourceStat.mode & 0o7777);
}
}
}
export async function materializeManagedWorktreeFixture(params: {
env: NodeJS.ProcessEnv;
name: string;
now: number;
ownerKind?: ManagedWorktreeOwnerKind;
ownerId?: string;
provisionedPaths?: readonly string[];
repoRoot: string;
stateDir: string;
}): Promise<ManagedWorktreeRecord> {
const repoFingerprint = "downstream-fixture";
const worktreePath = path.join(params.stateDir, "worktrees", repoFingerprint, params.name);
const branch = `openclaw/${params.name}`;
await fs.mkdir(path.dirname(worktreePath), { recursive: true });
await git(params.repoRoot, "worktree", "add", "-b", branch, "--", worktreePath, "HEAD");
const provisionedPaths = params.provisionedPaths ?? [];
await copyProvisionedFiles({
repoRoot: params.repoRoot,
worktreePath,
provisionedPaths,
});
const record: ManagedWorktreeRecord = {
id: `fixture-${params.name}`,
name: params.name,
repoFingerprint,
repoRoot: params.repoRoot,
path: worktreePath,
branch,
baseRef: "HEAD",
ownerKind: params.ownerKind ?? "manual",
...(params.ownerId ? { ownerId: params.ownerId } : {}),
createdAt: params.now,
lastActiveAt: params.now,
};
insertRegistryWorktree(params.env, record, { provisionedPaths });
return record;
}
+149 -145
View File
@@ -21,6 +21,7 @@ import {
resolveWorktreeCleanupLimits,
SNAPSHOT_RETENTION_MS,
} from "./service.js";
import { materializeManagedWorktreeFixture } from "./service.test-support.js";
const execFileAsync = promisify(execFile);
@@ -79,6 +80,34 @@ describe("ManagedWorktreeService", () => {
let env: NodeJS.ProcessEnv;
let now: number;
let service: ManagedWorktreeService;
let caseOrdinal = 0;
// Snapshot/removal/GC tests need the real Git-worktree + registry boundary,
// while create policy and provisioning composition stay covered above.
async function materializeDownstreamFixture(
name: string,
params: {
ownerKind?: "manual" | "session" | "workboard";
ownerId?: string;
provisionedPaths?: readonly string[];
repoRoot?: string;
} = {},
) {
return await materializeManagedWorktreeFixture({
env,
name,
now,
repoRoot: params.repoRoot ?? repo,
stateDir,
...params,
});
}
const materializeRunOwnedFixture = (
name: string,
ownerKind: "session" | "workboard",
ownerId?: string,
) => materializeDownstreamFixture(name, { ownerKind, ownerId });
beforeAll(async () => {
const tempRoot = await fs.realpath(os.tmpdir());
@@ -97,8 +126,8 @@ describe("ManagedWorktreeService", () => {
});
beforeEach(async () => {
const tempRoot = await fs.realpath(os.tmpdir());
root = await fs.mkdtemp(path.join(tempRoot, "openclaw-managed-worktrees-"));
root = path.join(templateRoot, `case-${caseOrdinal++}`);
await fs.mkdir(root);
repo = path.join(root, "repo");
await fs.cp(templateRepo, repo, {
mode: fsConstants.COPYFILE_FICLONE,
@@ -116,7 +145,6 @@ describe("ManagedWorktreeService", () => {
deleteRegistryWorktree(env, record.id);
}
await fs.rm(path.join(stateDir, "worktrees"), { recursive: true, force: true });
await fs.rm(root, { recursive: true, force: true });
});
it("creates from origin HEAD and returns the existing live named worktree", async () => {
@@ -135,6 +163,7 @@ describe("ManagedWorktreeService", () => {
const created = await service.create({
repoRoot: repo,
name: "session-owned",
baseRef: "HEAD",
ownerKind: "session",
ownerId: "session-1",
});
@@ -322,6 +351,7 @@ describe("ManagedWorktreeService", () => {
await service.create({
repoRoot: repo,
name: "shared-name",
baseRef: "HEAD",
ownerKind: "session",
ownerId: "agent:main:dashboard:one",
});
@@ -329,17 +359,19 @@ describe("ManagedWorktreeService", () => {
service.create({
repoRoot: repo,
name: "shared-name",
baseRef: "HEAD",
ownerKind: "session",
ownerId: "agent:main:dashboard:two",
}),
).rejects.toThrow(/already in use by session/);
await expect(service.create({ repoRoot: repo, name: "shared-name" })).rejects.toThrow(
/already in use by session/,
);
await expect(
service.create({ repoRoot: repo, name: "shared-name", baseRef: "HEAD" }),
).rejects.toThrow(/already in use by session/);
// The rightful owner still reuses its record.
const reused = await service.create({
repoRoot: repo,
name: "shared-name",
baseRef: "HEAD",
ownerKind: "session",
ownerId: "agent:main:dashboard:one",
});
@@ -377,7 +409,11 @@ describe("ManagedWorktreeService", () => {
const linked = path.join(root, "linked-source");
await git(repo, "worktree", "add", "-b", "linked-source", linked, "HEAD");
const linkedRoot = await fs.realpath(linked);
const created = await service.create({ repoRoot: linkedRoot, name: "linked-task" });
const created = await service.create({
repoRoot: linkedRoot,
name: "linked-task",
baseRef: "HEAD",
});
expect(created.repoRoot).toBe(repo);
await git(repo, "worktree", "remove", "--force", linkedRoot);
@@ -431,7 +467,7 @@ describe("ManagedWorktreeService", () => {
await fs.writeFile(path.join(outsideDir, "escape.txt"), "outside\n");
await fs.symlink(outsideDir, path.join(repo, "linked-dir"));
const created = await service.create({ repoRoot: repo, name: "includes" });
const created = await service.create({ repoRoot: repo, name: "includes", baseRef: "HEAD" });
const copied = path.join(created.path, "cache", "keep.txt");
expect(await fs.readFile(copied, "utf8")).toBe("keep\n");
expect((await fs.stat(copied)).mode & 0o777).toBe(0o744);
@@ -478,7 +514,7 @@ describe("ManagedWorktreeService", () => {
'#!/bin/sh\nprintf "%s\\n%s\\n" "$OPENCLAW_SOURCE_TREE_PATH" "$OPENCLAW_WORKTREE_PATH" > setup-paths.txt\n',
{ mode: 0o755 },
);
const created = await service.create({ repoRoot: repo, name: "setup" });
const created = await service.create({ repoRoot: repo, name: "setup", baseRef: "HEAD" });
expect(
(await fs.readFile(path.join(created.path, "setup-paths.txt"), "utf8")).split("\n"),
).toEqual([repo, created.path, ""]);
@@ -499,7 +535,12 @@ describe("ManagedWorktreeService", () => {
{ mode: 0o755 },
);
await service.create({ repoRoot: repo, name: "no-repo-code", runSetupScript: false });
await service.create({
repoRoot: repo,
name: "no-repo-code",
baseRef: "HEAD",
runSetupScript: false,
});
await expect(fs.access(hookMarker)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.access(setupMarker)).rejects.toMatchObject({ code: "ENOENT" });
@@ -509,9 +550,9 @@ describe("ManagedWorktreeService", () => {
await fs.mkdir(path.join(repo, ".openclaw"));
const script = path.join(repo, ".openclaw", "worktree-setup.sh");
await fs.writeFile(script, "#!/bin/sh\necho setup-broke >&2\nexit 9\n", { mode: 0o755 });
await expect(service.create({ repoRoot: repo, name: "broken-setup" })).rejects.toThrow(
"setup-broke",
);
await expect(
service.create({ repoRoot: repo, name: "broken-setup", baseRef: "HEAD" }),
).rejects.toThrow("setup-broke");
expect(await git(repo, "worktree", "list", "--porcelain")).not.toContain("broken-setup");
expect(await git(repo, "branch", "--list", "openclaw/broken-setup")).toBe("");
});
@@ -523,7 +564,9 @@ describe("ManagedWorktreeService", () => {
await git(repo, "commit", "-m", "configure worktree provisioning");
await fs.writeFile(path.join(repo, "provisioned.env"), "source value\n");
const mode = (await fs.stat(path.join(repo, "provisioned.env"))).mode & 0o7777;
const created = await service.create({ repoRoot: repo, name: "roundtrip" });
const created = await materializeDownstreamFixture("roundtrip", {
provisionedPaths: ["provisioned.env"],
});
const originalHead = await git(created.path, "rev-parse", "HEAD");
await fs.writeFile(path.join(created.path, "README.md"), "changed\n");
await fs.writeFile(path.join(created.path, "untracked.txt"), "untracked\n");
@@ -583,7 +626,7 @@ describe("ManagedWorktreeService", () => {
await git(repo, "commit", "-m", "add executable");
await git(repo, "config", "core.filemode", "false");
const created = await service.create({ repoRoot: repo, name: "filemode" });
const created = await materializeDownstreamFixture("filemode");
await fs.chmod(path.join(created.path, "tool.sh"), 0o644);
await fs.writeFile(path.join(created.path, "README.md"), "changed\n");
const removed = await service.remove({ id: created.id, reason: "test" });
@@ -592,7 +635,7 @@ describe("ManagedWorktreeService", () => {
});
it("snapshots modified tracked files marked assume-unchanged", async () => {
const created = await service.create({ repoRoot: repo, name: "assume-unchanged" });
const created = await materializeDownstreamFixture("assume-unchanged");
await git(created.path, "update-index", "--assume-unchanged", "README.md");
await fs.writeFile(path.join(created.path, "README.md"), "hidden local change\n");
expect(await git(created.path, "status", "--porcelain")).toBe("");
@@ -606,7 +649,7 @@ describe("ManagedWorktreeService", () => {
});
it("snapshots materialized tracked files marked skip-worktree", async () => {
const created = await service.create({ repoRoot: repo, name: "skip-worktree" });
const created = await materializeDownstreamFixture("skip-worktree");
await git(created.path, "update-index", "--skip-worktree", "README.md");
await fs.writeFile(path.join(created.path, "README.md"), "hidden sparse change\n");
expect(await git(created.path, "status", "--porcelain")).toBe("");
@@ -620,7 +663,7 @@ describe("ManagedWorktreeService", () => {
});
it("snapshots deletions hidden by skip-worktree outside sparse checkout", async () => {
const created = await service.create({ repoRoot: repo, name: "skip-worktree-deleted" });
const created = await materializeDownstreamFixture("skip-worktree-deleted");
await git(created.path, "update-index", "--skip-worktree", "README.md");
await fs.rm(path.join(created.path, "README.md"));
expect(await git(created.path, "status", "--porcelain")).toBe("");
@@ -634,7 +677,7 @@ describe("ManagedWorktreeService", () => {
});
it("refuses to overwrite a branch recreated before restore", async () => {
const created = await service.create({ repoRoot: repo, name: "restore-collision" });
const created = await materializeDownstreamFixture("restore-collision");
await service.remove({ id: created.id, reason: "test" });
await git(repo, "branch", created.branch, "HEAD");
const branchTip = await git(repo, "rev-parse", created.branch);
@@ -655,7 +698,7 @@ describe("ManagedWorktreeService", () => {
await fs.writeFile(path.join(repo, "tracked", "outer.txt"), "tracked\n");
await git(repo, "add", "tracked/outer.txt");
await git(repo, "commit", "-m", "add tracked parent");
const created = await service.create({ repoRoot: repo, name: "nested-repository" });
const created = await materializeDownstreamFixture("nested-repository");
const nested = await initializeRepository(
path.join(created.path, "tracked"),
gitTemplate,
@@ -684,6 +727,7 @@ describe("ManagedWorktreeService", () => {
const created = await service.create({
repoRoot: repo,
name: "wb-card",
baseRef: "HEAD",
ownerKind: "workboard",
ownerId: "card",
});
@@ -706,11 +750,11 @@ describe("ManagedWorktreeService", () => {
it("removes lossless run-end worktrees but keeps dirty and unpushed work", async () => {
await addRemote(root, repo);
const clean = await service.create({ repoRoot: repo, name: "clean" });
const clean = await materializeDownstreamFixture("clean");
await service.acquire(clean.id);
expect(await service.removeIfLossless(clean.id)).toBe(true);
const dirty = await service.create({ repoRoot: repo, name: "dirty" });
const dirty = await materializeDownstreamFixture("dirty");
await service.acquire(dirty.id);
await fs.writeFile(path.join(dirty.path, "dirty.txt"), "dirty\n");
expect(await service.removeIfLossless(dirty.id)).toBe(false);
@@ -718,7 +762,7 @@ describe("ManagedWorktreeService", () => {
(await service.list()).find((entry) => entry.id === dirty.id)?.removedAt,
).toBeUndefined();
const committed = await service.create({ repoRoot: repo, name: "committed" });
const committed = await materializeDownstreamFixture("committed");
await service.acquire(committed.id);
await fs.writeFile(path.join(committed.path, "commit.txt"), "commit\n");
await git(committed.path, "add", "commit.txt");
@@ -734,7 +778,9 @@ describe("ManagedWorktreeService", () => {
await fs.writeFile(path.join(repo, ".env.local"), "value=old-source\n");
await addRemote(root, repo);
const rotated = await service.create({ repoRoot: repo, name: "rotated-local" });
const rotated = await materializeDownstreamFixture("rotated-local", {
provisionedPaths: [".env.local"],
});
await service.acquire(rotated.id);
expect(await fs.readFile(path.join(rotated.path, ".env.local"), "utf8")).toBe(
"value=old-source\n",
@@ -748,13 +794,17 @@ describe("ManagedWorktreeService", () => {
"value=rotated-only-copy\n",
);
const rebuildable = await service.create({ repoRoot: repo, name: "rebuildable" });
const rebuildable = await materializeDownstreamFixture("rebuildable", {
provisionedPaths: [".env.local"],
});
await service.acquire(rebuildable.id);
await fs.mkdir(path.join(rebuildable.path, "node_modules"), { recursive: true });
await fs.writeFile(path.join(rebuildable.path, "node_modules", "cache.js"), "cache\n");
expect(await service.removeIfLossless(rebuildable.id)).toBe(true);
const deleted = await service.create({ repoRoot: repo, name: "deleted-local" });
const deleted = await materializeDownstreamFixture("deleted-local", {
provisionedPaths: [".env.local"],
});
await service.acquire(deleted.id);
const deletedCopy = path.join(deleted.path, ".env.local");
await fs.rm(deletedCopy);
@@ -776,7 +826,9 @@ describe("ManagedWorktreeService", () => {
await fs.writeFile(sourcePath, "value=source\n", { mode: 0o644 });
await addRemote(root, repo);
const executable = await service.create({ repoRoot: repo, name: "executable-local" });
const executable = await materializeDownstreamFixture("executable-local", {
provisionedPaths: [".env.local"],
});
await service.acquire(executable.id);
const executableCopy = path.join(executable.path, ".env.local");
await fs.chmod(executableCopy, 0o755);
@@ -787,7 +839,9 @@ describe("ManagedWorktreeService", () => {
0o755,
);
const specialMode = await service.create({ repoRoot: repo, name: "special-mode-local" });
const specialMode = await materializeDownstreamFixture("special-mode-local", {
provisionedPaths: [".env.local"],
});
await service.acquire(specialMode.id);
const specialModeCopy = path.join(specialMode.path, ".env.local");
await fs.chmod(specialModeCopy, 0o1644);
@@ -797,7 +851,9 @@ describe("ManagedWorktreeService", () => {
(await fs.lstat(path.join(restoredSpecialMode.path, ".env.local"))).mode & 0o7777,
).toBe(0o1644);
const linked = await service.create({ repoRoot: repo, name: "linked-local" });
const linked = await materializeDownstreamFixture("linked-local", {
provisionedPaths: [".env.local"],
});
await service.acquire(linked.id);
const linkedCopy = path.join(linked.path, ".env.local");
await fs.rm(linkedCopy);
@@ -806,7 +862,9 @@ describe("ManagedWorktreeService", () => {
expect(await service.removeIfLossless(linked.id)).toBe(false);
expect((await fs.lstat(linkedCopy)).isSymbolicLink()).toBe(true);
const sourceLinked = await service.create({ repoRoot: repo, name: "source-linked-local" });
const sourceLinked = await materializeDownstreamFixture("source-linked-local", {
provisionedPaths: [".env.local"],
});
await service.acquire(sourceLinked.id);
const outside = path.join(root, "same-local-value");
await fs.writeFile(outside, "value=source\n");
@@ -822,12 +880,8 @@ describe("ManagedWorktreeService", () => {
);
it("exempts manual worktrees and garbage collects idle run-owned worktrees", async () => {
const manual = await service.create({ repoRoot: repo, name: "manual-idle" });
const created = await service.create({
repoRoot: repo,
name: "idle-dead",
ownerKind: "workboard",
});
const manual = await materializeDownstreamFixture("manual-idle");
const created = await materializeRunOwnedFixture("idle-dead", "workboard");
await git(repo, "worktree", "lock", "--reason", "openclaw pid=999999", created.path);
now += IDLE_GC_MS + 1;
@@ -845,10 +899,9 @@ describe("ManagedWorktreeService", () => {
await git(repo, "commit", "-m", "configure worktree provisioning");
await fs.writeFile(path.join(repo, ".env.local"), "value=old-source\n");
const created = await service.create({
repoRoot: repo,
name: "idle-rotated",
const created = await materializeDownstreamFixture("idle-rotated", {
ownerKind: "workboard",
provisionedPaths: [".env.local"],
});
await fs.rm(path.join(repo, ".worktreeinclude"));
await fs.writeFile(path.join(created.path, ".env.local"), "value=rotated-only-copy\n");
@@ -863,18 +916,16 @@ describe("ManagedWorktreeService", () => {
});
it("uses owner activity to protect only active idle session worktrees", async () => {
const active = await service.create({
repoRoot: repo,
name: "active-session",
ownerKind: "session",
ownerId: "agent:main:active",
});
const inactive = await service.create({
repoRoot: repo,
name: "inactive-session",
ownerKind: "session",
ownerId: "agent:main:inactive",
});
const active = await materializeRunOwnedFixture(
"active-session",
"session",
"agent:main:active",
);
const inactive = await materializeRunOwnedFixture(
"inactive-session",
"session",
"agent:main:inactive",
);
now += IDLE_GC_MS + 1;
const shouldProtectOwner = vi.fn(
(_ownerKind: string, ownerId: string) => ownerId === "agent:main:active",
@@ -890,11 +941,7 @@ describe("ManagedWorktreeService", () => {
});
it("protects foreign locks during idle garbage collection", async () => {
const created = await service.create({
repoRoot: repo,
name: "foreign-lock",
ownerKind: "session",
});
const created = await materializeRunOwnedFixture("foreign-lock", "session");
await git(repo, "worktree", "lock", "--reason", "other-tool", created.path);
now += IDLE_GC_MS + 1;
@@ -903,17 +950,9 @@ describe("ManagedWorktreeService", () => {
});
it("continues garbage collection after one worktree cannot be snapshotted", async () => {
const removable = await service.create({
repoRoot: repo,
name: "removable",
ownerKind: "workboard",
});
const removable = await materializeRunOwnedFixture("removable", "workboard");
now += 1;
const nestedRecord = await service.create({
repoRoot: repo,
name: "nested-idle",
ownerKind: "workboard",
});
const nestedRecord = await materializeRunOwnedFixture("nested-idle", "workboard");
await initializeRepository(nestedRecord.path, gitTemplate, "nested");
now += IDLE_GC_MS + 1;
@@ -926,15 +965,12 @@ describe("ManagedWorktreeService", () => {
it("continues garbage collection when one repository control path is missing", async () => {
const otherRepo = await initializeRepository(root, gitTemplate, "other-repo");
const removable = await service.create({
const removable = await materializeDownstreamFixture("other-removable", {
repoRoot: otherRepo,
name: "other-removable",
ownerKind: "session",
});
now += 1;
const broken = await service.create({
repoRoot: repo,
name: "missing-control",
const broken = await materializeDownstreamFixture("missing-control", {
ownerKind: "session",
});
await fs.rename(repo, path.join(root, "moved-repo"));
@@ -962,27 +998,12 @@ describe("ManagedWorktreeService", () => {
});
it("evicts the least recently active run-owned worktrees over the count limit", async () => {
const manual = await service.create({ repoRoot: repo, name: "manual-kept" });
const oldest = await service.create({
repoRoot: repo,
name: "count-oldest",
ownerKind: "session",
ownerId: "agent:main:oldest",
});
const manual = await materializeDownstreamFixture("manual-kept");
const oldest = await materializeRunOwnedFixture("count-oldest", "session", "agent:main:oldest");
now += 1;
const middle = await service.create({
repoRoot: repo,
name: "count-middle",
ownerKind: "workboard",
ownerId: "card-middle",
});
const middle = await materializeRunOwnedFixture("count-middle", "workboard", "card-middle");
now += 1;
const newest = await service.create({
repoRoot: repo,
name: "count-newest",
ownerKind: "session",
ownerId: "agent:main:newest",
});
const newest = await materializeRunOwnedFixture("count-newest", "session", "agent:main:newest");
const result = await service.gc({ limits: { maxCount: 2 } });
@@ -994,19 +1015,13 @@ describe("ManagedWorktreeService", () => {
});
it("skips active owners during count-limit eviction", async () => {
const activeOldest = await service.create({
repoRoot: repo,
name: "limit-active",
ownerKind: "session",
ownerId: "agent:main:active",
});
const activeOldest = await materializeRunOwnedFixture(
"limit-active",
"session",
"agent:main:active",
);
now += 1;
const idle = await service.create({
repoRoot: repo,
name: "limit-idle",
ownerKind: "session",
ownerId: "agent:main:idle",
});
const idle = await materializeRunOwnedFixture("limit-idle", "session", "agent:main:idle");
const shouldProtectOwner = vi.fn(
(_ownerKind: string, ownerId: string) => ownerId === "agent:main:active",
);
@@ -1018,20 +1033,18 @@ describe("ManagedWorktreeService", () => {
});
it("evicts oldest worktrees until total size fits the size limit", async () => {
const oldest = await service.create({
repoRoot: repo,
name: "size-oldest",
ownerKind: "session",
ownerId: "agent:main:size-old",
});
const oldest = await materializeRunOwnedFixture(
"size-oldest",
"session",
"agent:main:size-old",
);
await fs.writeFile(path.join(oldest.path, "blob.bin"), Buffer.alloc(10_000));
now += 1;
const newest = await service.create({
repoRoot: repo,
name: "size-newest",
ownerKind: "session",
ownerId: "agent:main:size-new",
});
const newest = await materializeRunOwnedFixture(
"size-newest",
"session",
"agent:main:size-new",
);
const result = await service.gc({ limits: { maxTotalSizeBytes: 6_000 } });
@@ -1044,12 +1057,11 @@ describe("ManagedWorktreeService", () => {
if (process.getuid?.() === 0) {
return; // chmod-based EACCES cannot be simulated as root
}
const unreadable = await service.create({
repoRoot: repo,
name: "size-unreadable",
ownerKind: "session",
ownerId: "agent:main:size-unreadable",
});
const unreadable = await materializeRunOwnedFixture(
"size-unreadable",
"session",
"agent:main:size-unreadable",
);
await fs.writeFile(path.join(unreadable.path, "blob.bin"), Buffer.alloc(10_000));
const locked = path.join(unreadable.path, "locked");
await fs.mkdir(locked);
@@ -1066,26 +1078,23 @@ describe("ManagedWorktreeService", () => {
});
it("counts a competing removal instead of evicting an extra worktree", async () => {
const oldest = await service.create({
repoRoot: repo,
name: "race-oldest",
ownerKind: "session",
ownerId: "agent:main:race-old",
});
const oldest = await materializeRunOwnedFixture(
"race-oldest",
"session",
"agent:main:race-old",
);
now += 1;
const middle = await service.create({
repoRoot: repo,
name: "race-middle",
ownerKind: "session",
ownerId: "agent:main:race-mid",
});
const middle = await materializeRunOwnedFixture(
"race-middle",
"session",
"agent:main:race-mid",
);
now += 1;
const newest = await service.create({
repoRoot: repo,
name: "race-newest",
ownerKind: "session",
ownerId: "agent:main:race-new",
});
const newest = await materializeRunOwnedFixture(
"race-newest",
"session",
"agent:main:race-new",
);
const realRemove = service.remove.bind(service);
const removeSpy = vi
.spyOn(service, "remove")
@@ -1107,12 +1116,7 @@ describe("ManagedWorktreeService", () => {
});
it("leaves everything in place when limits are not exceeded", async () => {
const created = await service.create({
repoRoot: repo,
name: "under-limit",
ownerKind: "session",
ownerId: "agent:main:under",
});
const created = await materializeRunOwnedFixture("under-limit", "session", "agent:main:under");
const result = await service.gc({
limits: { maxCount: 5, maxTotalSizeBytes: 1024 ** 3 },
@@ -1127,7 +1131,7 @@ describe("ManagedWorktreeService", () => {
});
it("prunes expired snapshot refs and registry rows", async () => {
const created = await service.create({ repoRoot: repo, name: "expired" });
const created = await materializeDownstreamFixture("expired");
const removed = await service.remove({ id: created.id, reason: "retention" });
now += SNAPSHOT_RETENTION_MS + 1;