diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index 8493f897..ee13b54a 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -2,13 +2,14 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { copyGitHooksToWorktreeGitDir, copySeededSecretsKey, rebindWorkspaceCwd, resolveGitWorktreeAddArgs, resolveWorktreeMakeTargetPath, + worktreeInitCommand, worktreeMakeCommand, } from "../commands/worktree.js"; import { @@ -189,7 +190,11 @@ describe("worktree helpers", () => { it("copies the source local_encrypted secrets key into the seeded worktree instance", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-")); + const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY; + const originalKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; try { + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY; + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const sourceKeyPath = path.join(tempRoot, "source", "secrets", "master.key"); const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key"); @@ -208,6 +213,16 @@ describe("worktree helpers", () => { expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("source-master-key"); } finally { + if (originalInlineMasterKey === undefined) { + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY; + } else { + process.env.PAPERCLIP_SECRETS_MASTER_KEY = originalInlineMasterKey; + } + if (originalKeyFile === undefined) { + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + } else { + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = originalKeyFile; + } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); @@ -233,6 +248,36 @@ describe("worktree helpers", () => { } }); + it("persists the current agent jwt secret into the worktree env file", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-jwt-")); + const repoRoot = path.join(tempRoot, "repo"); + const originalCwd = process.cwd(); + const originalJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET; + + try { + fs.mkdirSync(repoRoot, { recursive: true }); + process.env.PAPERCLIP_AGENT_JWT_SECRET = "worktree-shared-secret"; + process.chdir(repoRoot); + + await worktreeInitCommand({ + seed: false, + fromConfig: path.join(tempRoot, "missing", "config.json"), + home: path.join(tempRoot, ".paperclip-worktrees"), + }); + + const envPath = path.join(repoRoot, ".paperclip", ".env"); + expect(fs.readFileSync(envPath, "utf8")).toContain("PAPERCLIP_AGENT_JWT_SECRET=worktree-shared-secret"); + } finally { + process.chdir(originalCwd); + if (originalJwtSecret === undefined) { + delete process.env.PAPERCLIP_AGENT_JWT_SECRET; + } else { + process.env.PAPERCLIP_AGENT_JWT_SECRET = originalJwtSecret; + } + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("rebinds same-repo workspace paths onto the current worktree root", () => { expect( rebindWorkspaceCwd({ @@ -315,7 +360,7 @@ describe("worktree helpers", () => { const fakeHome = path.join(tempRoot, "home"); const worktreePath = path.join(fakeHome, "paperclip-make-test"); const originalCwd = process.cwd(); - const originalHome = process.env.HOME; + const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(fakeHome); try { fs.mkdirSync(repoRoot, { recursive: true }); @@ -327,7 +372,6 @@ describe("worktree helpers", () => { execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); - process.env.HOME = fakeHome; process.chdir(repoRoot); await worktreeMakeCommand("paperclip-make-test", { @@ -340,12 +384,8 @@ describe("worktree helpers", () => { expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true); } finally { process.chdir(originalCwd); - if (originalHome === undefined) { - delete process.env.HOME; - } else { - process.env.HOME = originalHome; - } + homedirSpy.mockRestore(); fs.rmSync(tempRoot, { recursive: true, force: true }); } - }); + }, 20_000); }); diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index 8781b008..f1552329 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -642,7 +642,17 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { }); writeConfig(targetConfig, paths.configPath); - mergePaperclipEnvEntries(buildWorktreeEnvEntries(paths), paths.envPath); + const sourceEnvEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(sourceConfigPath)); + const existingAgentJwtSecret = + nonEmpty(sourceEnvEntries.PAPERCLIP_AGENT_JWT_SECRET) ?? + nonEmpty(process.env.PAPERCLIP_AGENT_JWT_SECRET); + mergePaperclipEnvEntries( + { + ...buildWorktreeEnvEntries(paths), + ...(existingAgentJwtSecret ? { PAPERCLIP_AGENT_JWT_SECRET: existingAgentJwtSecret } : {}), + }, + paths.envPath, + ); ensureAgentJwtSecret(paths.configPath); loadPaperclipEnvFile(paths.configPath); const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd); diff --git a/server/src/__tests__/private-hostname-guard.test.ts b/server/src/__tests__/private-hostname-guard.test.ts index e0e74d56..4879e279 100644 --- a/server/src/__tests__/private-hostname-guard.test.ts +++ b/server/src/__tests__/private-hostname-guard.test.ts @@ -52,5 +52,5 @@ describe("privateHostnameGuard", () => { const res = await request(app).get("/dashboard").set("Host", "dotta-macbook-pro:3100"); expect(res.status).toBe(403); expect(res.text).toContain("please run pnpm paperclipai allowed-hostname dotta-macbook-pro"); - }); + }, 20_000); });