diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index 9fbd3ccc6e3..81c4aff423d 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -53,6 +53,24 @@ run_paperclipai_command() { return 1 } +paperclipai_command_available() { + if command -v pnpm >/dev/null 2>&1 && pnpm paperclipai --help >/dev/null 2>&1; then + return 0 + fi + + local base_cli_tsx_path="$base_cwd/cli/node_modules/tsx/dist/cli.mjs" + local base_cli_entry_path="$base_cwd/cli/src/index.ts" + if command -v node >/dev/null 2>&1 && [[ -f "$base_cli_tsx_path" ]] && [[ -f "$base_cli_entry_path" ]]; then + return 0 + fi + + if command -v paperclipai >/dev/null 2>&1; then + return 0 + fi + + return 1 +} + run_isolated_worktree_init() { run_paperclipai_command \ worktree \ @@ -318,7 +336,9 @@ main().catch((error) => { EOF } -if ! run_isolated_worktree_init; then +if paperclipai_command_available; then + run_isolated_worktree_init +else echo "paperclipai CLI not available in this workspace; writing isolated fallback config without DB seeding." >&2 write_fallback_worktree_config fi diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 6e2287a012a..ca11225e7bc 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -1134,6 +1134,62 @@ describe("realizeExecutionWorkspace", () => { ); }, 30_000); + it("fails instead of writing an unseeded fallback config when worktree init errors after CLI detection succeeds", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-provision-fail-")); + const baseRoot = path.join(tempRoot, "base"); + const worktreeRoot = path.join(tempRoot, "worktree"); + const fakeBin = path.join(tempRoot, "bin"); + const fakePnpmPath = path.join(fakeBin, "pnpm"); + const scriptPath = path.join(worktreeRoot, "provision-worktree.sh"); + + try { + await fs.mkdir(baseRoot, { recursive: true }); + await fs.mkdir(worktreeRoot, { recursive: true }); + await fs.mkdir(fakeBin, { recursive: true }); + await fs.copyFile(provisionWorktreeScriptPath, scriptPath); + await fs.chmod(scriptPath, 0o755); + await fs.writeFile( + fakePnpmPath, + [ + "#!/bin/sh", + "if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"--help\" ]; then", + " exit 0", + "fi", + "if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"worktree\" ] && [ \"$3\" = \"init\" ]; then", + " echo \"simulated init failure\" >&2", + " exit 42", + "fi", + "exit 0", + "", + ].join("\n"), + "utf8", + ); + await fs.chmod(fakePnpmPath, 0o755); + + let caught: Error | null = null; + try { + await execFileAsync(scriptPath, [], { + cwd: worktreeRoot, + env: { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + PAPERCLIP_WORKSPACE_BASE_CWD: baseRoot, + PAPERCLIP_WORKSPACE_CWD: worktreeRoot, + }, + }); + } catch (error) { + caught = error as Error; + } + + expect(caught).toBeTruthy(); + expect(String(caught)).toContain("simulated init failure"); + await expect(fs.stat(path.join(worktreeRoot, ".paperclip", "config.json"))).rejects.toThrow(); + await expect(fs.stat(path.join(worktreeRoot, ".paperclip", ".env"))).rejects.toThrow(); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + it( "provisions worktree-local pnpm node_modules instead of reusing base-repo links", async () => {