Repair stale worktree links before runtime start

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-04 20:02:04 -05:00
parent 08fea10ce1
commit 8be6fe987b
2 changed files with 245 additions and 2 deletions

View file

@ -20,6 +20,7 @@ import {
import { eq } from "drizzle-orm";
import {
cleanupExecutionWorkspaceArtifacts,
ensureServerWorkspaceLinksCurrent,
ensureRuntimeServicesForRun,
normalizeAdapterManagedRuntimeServices,
reconcilePersistedRuntimeServicesOnStartup,
@ -187,6 +188,96 @@ describe("sanitizeRuntimeServiceBaseEnv", () => {
});
});
describe("ensureServerWorkspaceLinksCurrent", () => {
it("relinks stale server workspace dependencies inside the current repo root", async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-"));
const staleRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-stale-"));
const serverNodeModulesScopeDir = path.join(repoRoot, "server", "node_modules", "@paperclipai");
const expectedPackageDir = path.join(repoRoot, "packages", "db");
const stalePackageDir = path.join(staleRoot, "db");
await fs.mkdir(path.join(repoRoot, "server"), { recursive: true });
await fs.mkdir(expectedPackageDir, { recursive: true });
await fs.mkdir(stalePackageDir, { recursive: true });
await fs.mkdir(serverNodeModulesScopeDir, { recursive: true });
await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8");
await fs.writeFile(
path.join(repoRoot, "server", "package.json"),
JSON.stringify({
name: "@paperclipai/server",
dependencies: {
"@paperclipai/db": "workspace:*",
},
}),
"utf8",
);
await fs.writeFile(
path.join(expectedPackageDir, "package.json"),
JSON.stringify({ name: "@paperclipai/db" }),
"utf8",
);
await fs.writeFile(
path.join(stalePackageDir, "package.json"),
JSON.stringify({ name: "@paperclipai/db" }),
"utf8",
);
await fs.symlink(stalePackageDir, path.join(serverNodeModulesScopeDir, "db"));
const commands: Array<{ command: string; args: string[]; cwd: string }> = [];
await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), {
runCommand: async (command, args, cwd) => {
commands.push({ command, args, cwd });
await fs.rm(path.join(serverNodeModulesScopeDir, "db"), { force: true });
await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db"));
},
});
expect(commands).toHaveLength(1);
expect(commands[0]).toMatchObject({
command: process.platform === "win32" ? "pnpm.cmd" : "pnpm",
args: ["install", "--force", "--config.confirmModulesPurge=false"],
cwd: repoRoot,
});
expect(await fs.realpath(path.join(serverNodeModulesScopeDir, "db"))).toBe(await fs.realpath(expectedPackageDir));
});
it("skips relinking when server workspace dependencies already point at the repo", async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-current-"));
const serverNodeModulesScopeDir = path.join(repoRoot, "server", "node_modules", "@paperclipai");
const expectedPackageDir = path.join(repoRoot, "packages", "db");
await fs.mkdir(path.join(repoRoot, "server"), { recursive: true });
await fs.mkdir(expectedPackageDir, { recursive: true });
await fs.mkdir(serverNodeModulesScopeDir, { recursive: true });
await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8");
await fs.writeFile(
path.join(repoRoot, "server", "package.json"),
JSON.stringify({
name: "@paperclipai/server",
dependencies: {
"@paperclipai/db": "workspace:*",
},
}),
"utf8",
);
await fs.writeFile(
path.join(expectedPackageDir, "package.json"),
JSON.stringify({ name: "@paperclipai/db" }),
"utf8",
);
await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db"));
let invoked = false;
await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), {
runCommand: async () => {
invoked = true;
},
});
expect(invoked).toBe(false);
});
});
describe("realizeExecutionWorkspace", () => {
it("creates and reuses a git worktree for an issue-scoped branch", async () => {
const repoRoot = await createTempRepo();
@ -663,7 +754,7 @@ describe("realizeExecutionWorkspace", () => {
await fs.realpath(path.join(repoRoot, "packages", "shared")),
);
},
15_000,
30_000,
);
it("records worktree setup and provision operations when a recorder is provided", async () => {