import { execFile } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { agents, companies, createDb, executionWorkspaces, heartbeatRuns, projects, workspaceRuntimeServices, } from "@paperclipai/db"; import { eq } from "drizzle-orm"; import { cleanupExecutionWorkspaceArtifacts, ensureRuntimeServicesForRun, normalizeAdapterManagedRuntimeServices, reconcilePersistedRuntimeServicesOnStartup, realizeExecutionWorkspace, releaseRuntimeServicesForRun, resetRuntimeServicesForTests, sanitizeRuntimeServiceBaseEnv, stopRuntimeServicesForExecutionWorkspace, type RealizedExecutionWorkspace, } from "../services/workspace-runtime.ts"; import { resolvePaperclipConfigPath } from "../paths.ts"; import type { WorkspaceOperation } from "@paperclipai/shared"; import type { WorkspaceOperationRecorder } from "../services/workspace-operations.ts"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; const execFileAsync = promisify(execFile); const leasedRunIds = new Set(); const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres workspace-runtime tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } async function runGit(cwd: string, args: string[]) { await execFileAsync("git", args, { cwd }); } async function createTempRepo(defaultBranch = "main") { const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-repo-")); await runGit(repoRoot, ["init"]); await runGit(repoRoot, ["config", "user.email", "paperclip@example.com"]); await runGit(repoRoot, ["config", "user.name", "Paperclip Test"]); await fs.writeFile(path.join(repoRoot, "README.md"), "hello\n", "utf8"); await runGit(repoRoot, ["add", "README.md"]); await runGit(repoRoot, ["commit", "-m", "Initial commit"]); await runGit(repoRoot, ["checkout", "-B", defaultBranch]); return repoRoot; } function buildWorkspace(cwd: string): RealizedExecutionWorkspace { return { baseCwd: cwd, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", strategy: "project_primary", cwd, branchName: null, worktreePath: null, warnings: [], created: false, }; } function createWorkspaceOperationRecorderDouble() { const operations: Array<{ phase: string; command: string | null; cwd: string | null; metadata: Record | null; result: { status?: string; exitCode?: number | null; stdout?: string | null; stderr?: string | null; system?: string | null; metadata?: Record | null; }; }> = []; let executionWorkspaceId: string | null = null; const recorder: WorkspaceOperationRecorder = { attachExecutionWorkspaceId: async (nextExecutionWorkspaceId) => { executionWorkspaceId = nextExecutionWorkspaceId; }, recordOperation: async (input) => { const result = await input.run(); operations.push({ phase: input.phase, command: input.command ?? null, cwd: input.cwd ?? null, metadata: { ...(input.metadata ?? {}), ...(executionWorkspaceId ? { executionWorkspaceId } : {}), }, result, }); return { id: `op-${operations.length}`, companyId: "company-1", executionWorkspaceId, heartbeatRunId: "run-1", phase: input.phase, command: input.command ?? null, cwd: input.cwd ?? null, status: (result.status ?? "succeeded") as WorkspaceOperation["status"], exitCode: result.exitCode ?? null, logStore: "local_file", logRef: `op-${operations.length}.ndjson`, logBytes: 0, logSha256: null, logCompressed: false, stdoutExcerpt: result.stdout ?? null, stderrExcerpt: result.stderr ?? null, metadata: input.metadata ?? null, startedAt: new Date(), finishedAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }; }, }; return { recorder, operations }; } afterEach(async () => { await Promise.all( Array.from(leasedRunIds).map(async (runId) => { await releaseRuntimeServicesForRun(runId); leasedRunIds.delete(runId); }), ); delete process.env.PAPERCLIP_CONFIG; delete process.env.PAPERCLIP_HOME; delete process.env.PAPERCLIP_INSTANCE_ID; delete process.env.PAPERCLIP_WORKTREES_DIR; delete process.env.DATABASE_URL; await resetRuntimeServicesForTests(); }); describe("sanitizeRuntimeServiceBaseEnv", () => { it("removes inherited Paperclip and pnpm auth flags before spawning runtime services", () => { const sanitized = sanitizeRuntimeServiceBaseEnv({ PATH: process.env.PATH, DATABASE_URL: "postgres://example.test/paperclip", PAPERCLIP_HOME: "/tmp/paperclip-home", PAPERCLIP_INSTANCE_ID: "runtime-instance", npm_config_tailscale_auth: "true", npm_config_authenticated_private: "true", HOST: "0.0.0.0", }); expect(sanitized.PAPERCLIP_HOME).toBeUndefined(); expect(sanitized.PAPERCLIP_INSTANCE_ID).toBeUndefined(); expect(sanitized.DATABASE_URL).toBeUndefined(); expect(sanitized.npm_config_tailscale_auth).toBeUndefined(); expect(sanitized.npm_config_authenticated_private).toBeUndefined(); expect(sanitized.HOST).toBe("0.0.0.0"); }); }); describe("realizeExecutionWorkspace", () => { it("creates and reuses a git worktree for an issue-scoped branch", async () => { const repoRoot = await createTempRepo(); const first = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-1", identifier: "PAP-447", title: "Add Worktree Support", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); expect(first.strategy).toBe("git_worktree"); expect(first.created).toBe(true); expect(first.branchName).toBe("PAP-447-add-worktree-support"); expect(first.cwd).toContain(path.join(".paperclip", "worktrees")); await expect(fs.stat(path.join(first.cwd, ".git"))).resolves.toBeTruthy(); const second = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-1", identifier: "PAP-447", title: "Add Worktree Support", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); expect(second.created).toBe(false); expect(second.cwd).toBe(first.cwd); expect(second.branchName).toBe(first.branchName); }); it("slugifies unsafe issue titles for branch names and worktree folders", async () => { const repoRoot = await createTempRepo(); const realized = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-unsafe", identifier: "PAP-991", title: "there should be a setting for the allowance of thumbs up / thumbs down data; `rm -rf`", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); expect(realized.branchName).toBe( "PAP-991-there-should-be-a-setting-for-the-allowance-of-thumbs-up-thumbs-down-data-rm-rf", ); expect(realized.branchName?.includes("/")).toBe(false); expect(path.basename(realized.cwd)).toBe(realized.branchName); }); it("preserves intentional slashes and dots from the branch template", async () => { const repoRoot = await createTempRepo(); const realized = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "release/{{issue.identifier}}.{{slug}}", }, }, issue: { id: "issue-template-safe", identifier: "PAP-992", title: "Hotfix / April.1", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); expect(realized.branchName).toBe("release/PAP-992.hotfix-april-1"); expect(path.basename(realized.cwd)).toBe("PAP-992.hotfix-april-1"); }); it("runs a configured provision command inside the derived worktree", async () => { const repoRoot = await createTempRepo(); await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); await fs.writeFile( path.join(repoRoot, "scripts", "provision.sh"), [ "#!/usr/bin/env bash", "set -euo pipefail", "printf '%s\\n' \"$PAPERCLIP_WORKSPACE_BRANCH\" > .paperclip-provision-branch", "printf '%s\\n' \"$PAPERCLIP_WORKSPACE_BASE_CWD\" > .paperclip-provision-base", "printf '%s\\n' \"$PAPERCLIP_WORKSPACE_CREATED\" > .paperclip-provision-created", ].join("\n"), "utf8", ); await runGit(repoRoot, ["add", "scripts/provision.sh"]); await runGit(repoRoot, ["commit", "-m", "Add worktree provision script"]); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", provisionCommand: "bash ./scripts/provision.sh", }, }, issue: { id: "issue-1", identifier: "PAP-448", title: "Run provision command", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); await expect(fs.readFile(path.join(workspace.cwd, ".paperclip-provision-branch"), "utf8")).resolves.toBe( "PAP-448-run-provision-command\n", ); await expect(fs.readFile(path.join(workspace.cwd, ".paperclip-provision-base"), "utf8")).resolves.toBe( `${repoRoot}\n`, ); await expect(fs.readFile(path.join(workspace.cwd, ".paperclip-provision-created"), "utf8")).resolves.toBe( "true\n", ); const reused = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", provisionCommand: "bash ./scripts/provision.sh", }, }, issue: { id: "issue-1", identifier: "PAP-448", title: "Run provision command", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); await expect(fs.readFile(path.join(reused.cwd, ".paperclip-provision-created"), "utf8")).resolves.toBe("false\n"); }); it("writes an isolated repo-local Paperclip config and worktree branding when provisioning", async () => { const repoRoot = await createTempRepo(); const previousCwd = process.cwd(); const paperclipHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-home-")); const isolatedWorktreeHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktrees-")); const instanceId = "worktree-base"; const sharedConfigDir = path.join(paperclipHome, "instances", instanceId); const sharedConfigPath = path.join(sharedConfigDir, "config.json"); const sharedEnvPath = path.join(sharedConfigDir, ".env"); process.env.PAPERCLIP_HOME = paperclipHome; process.env.PAPERCLIP_INSTANCE_ID = instanceId; process.env.PAPERCLIP_WORKTREES_DIR = isolatedWorktreeHome; await fs.mkdir(sharedConfigDir, { recursive: true }); await fs.writeFile( sharedConfigPath, JSON.stringify( { $meta: { version: 1, updatedAt: "2026-03-26T00:00:00.000Z", source: "doctor", }, database: { mode: "embedded-postgres", embeddedPostgresDataDir: path.join(sharedConfigDir, "db"), embeddedPostgresPort: 54329, backup: { enabled: true, intervalMinutes: 60, retentionDays: 30, dir: path.join(sharedConfigDir, "backups"), }, }, logging: { mode: "file", logDir: path.join(sharedConfigDir, "logs"), }, server: { deploymentMode: "local_trusted", exposure: "private", host: "127.0.0.1", port: 3100, allowedHostnames: [], serveUi: true, }, auth: { baseUrlMode: "auto", disableSignUp: false, }, storage: { provider: "local_disk", localDisk: { baseDir: path.join(sharedConfigDir, "storage"), }, s3: { bucket: "paperclip", region: "us-east-1", prefix: "", forcePathStyle: false, }, }, secrets: { provider: "local_encrypted", strictMode: false, localEncrypted: { keyFilePath: path.join(sharedConfigDir, "master.key"), }, }, }, null, 2, ) + "\n", "utf8", ); await fs.writeFile(sharedEnvPath, 'DATABASE_URL="postgres://worktree:test@db.example.com:6543/paperclip"\n', "utf8"); await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); await fs.copyFile( fileURLToPath(new URL("../../../scripts/provision-worktree.sh", import.meta.url)), path.join(repoRoot, "scripts", "provision-worktree.sh"), ); await runGit(repoRoot, ["add", "scripts/provision-worktree.sh"]); await runGit(repoRoot, ["commit", "-m", "Add worktree provision script"]); try { const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", provisionCommand: "bash ./scripts/provision-worktree.sh", }, }, issue: { id: "issue-1", identifier: "PAP-885", title: "Show worktree banner", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); const configPath = path.join(workspace.cwd, ".paperclip", "config.json"); const envPath = path.join(workspace.cwd, ".paperclip", ".env"); const envContents = await fs.readFile(envPath, "utf8"); const configContents = JSON.parse(await fs.readFile(configPath, "utf8")); const configStats = await fs.lstat(configPath); const expectedInstanceId = "pap-885-show-worktree-banner"; const expectedInstanceRoot = path.join( isolatedWorktreeHome, "instances", expectedInstanceId, ); expect(configStats.isSymbolicLink()).toBe(false); expect(configContents.database.embeddedPostgresDataDir).toBe(path.join(expectedInstanceRoot, "db")); expect(configContents.database.embeddedPostgresDataDir).not.toBe(path.join(sharedConfigDir, "db")); expect(configContents.server.port).not.toBe(3100); expect(configContents.secrets.localEncrypted.keyFilePath).toBe( path.join(expectedInstanceRoot, "secrets", "master.key"), ); expect(envContents).not.toContain("DATABASE_URL="); expect(envContents).toContain(`PAPERCLIP_HOME=${JSON.stringify(isolatedWorktreeHome)}`); expect(envContents).toContain(`PAPERCLIP_INSTANCE_ID=${JSON.stringify(expectedInstanceId)}`); expect(envContents).toContain(`PAPERCLIP_CONFIG=${JSON.stringify(configPath)}`); expect(envContents).toContain("PAPERCLIP_IN_WORKTREE=true"); expect(envContents).toContain( `PAPERCLIP_WORKTREE_NAME=${JSON.stringify("PAP-885-show-worktree-banner")}`, ); process.chdir(workspace.cwd); expect(resolvePaperclipConfigPath()).toBe(configPath); } finally { process.chdir(previousCwd); } }); it("records worktree setup and provision operations when a recorder is provided", async () => { const repoRoot = await createTempRepo(); const { recorder, operations } = createWorkspaceOperationRecorderDouble(); await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); await fs.writeFile( path.join(repoRoot, "scripts", "provision.sh"), [ "#!/usr/bin/env bash", "set -euo pipefail", "printf 'provisioned\\n'", ].join("\n"), "utf8", ); await runGit(repoRoot, ["add", "scripts/provision.sh"]); await runGit(repoRoot, ["commit", "-m", "Add recorder provision script"]); await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", provisionCommand: "bash ./scripts/provision.sh", }, }, issue: { id: "issue-1", identifier: "PAP-540", title: "Record workspace operations", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, recorder, }); expect(operations.map((operation) => operation.phase)).toEqual([ "worktree_prepare", "workspace_provision", ]); expect(operations[0]?.command).toContain("git worktree add"); expect(operations[0]?.metadata).toMatchObject({ branchName: "PAP-540-record-workspace-operations", created: true, }); expect(operations[1]?.command).toBe("bash ./scripts/provision.sh"); }); it("reuses an existing branch without resetting it when recreating a missing worktree", async () => { const repoRoot = await createTempRepo(); const branchName = "PAP-450-recreate-missing-worktree"; await runGit(repoRoot, ["checkout", "-b", branchName]); await fs.writeFile(path.join(repoRoot, "feature.txt"), "preserve me\n", "utf8"); await runGit(repoRoot, ["add", "feature.txt"]); await runGit(repoRoot, ["commit", "-m", "Add preserved feature"]); const expectedHead = (await execFileAsync("git", ["rev-parse", branchName], { cwd: repoRoot })).stdout.trim(); await runGit(repoRoot, ["checkout", "main"]); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-1", identifier: "PAP-450", title: "Recreate missing worktree", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); expect(workspace.branchName).toBe(branchName); await expect(fs.readFile(path.join(workspace.cwd, "feature.txt"), "utf8")).resolves.toBe("preserve me\n"); const actualHead = (await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: workspace.cwd })).stdout.trim(); expect(actualHead).toBe(expectedHead); }); it("auto-detects the default branch when baseRef is not configured", async () => { // Create a repo with "master" as default branch (not "main") const repoRoot = await createTempRepo("master"); // Set up a bare remote and push master so refs/remotes/origin/master // exists locally. Note: refs/remotes/origin/HEAD is NOT set by a manual // fetch — that requires git clone or git remote set-head. This test // exercises the heuristic fallback path in detectDefaultBranch. const bareRemote = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-bare-")); await runGit(bareRemote, ["init", "--bare"]); await runGit(repoRoot, ["remote", "add", "origin", bareRemote]); await runGit(repoRoot, ["push", "-u", "origin", "master"]); await runGit(repoRoot, ["fetch", "origin"]); const { recorder, operations } = createWorkspaceOperationRecorderDouble(); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: null, }, config: { workspaceStrategy: { type: "git_worktree", // No baseRef configured — should auto-detect "master" }, }, issue: { id: "issue-1", identifier: "PAP-460", title: "Auto detect default branch", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, recorder, }); expect(workspace.strategy).toBe("git_worktree"); expect(workspace.created).toBe(true); // The worktree should have been created successfully (baseRef resolved to "master") const worktreeOp = operations.find(op => op.phase === "worktree_prepare" && op.metadata?.created); expect(worktreeOp).toBeDefined(); expect(worktreeOp!.metadata!.baseRef).toBe("master"); }); it("auto-detects the default branch via symbolic-ref when origin/HEAD is set", async () => { // Create a repo with "master" as default branch const repoRoot = await createTempRepo("master"); // Set up a bare remote and push const bareRemote = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-bare-symref-")); await runGit(bareRemote, ["init", "--bare"]); await runGit(repoRoot, ["remote", "add", "origin", bareRemote]); await runGit(repoRoot, ["push", "-u", "origin", "master"]); await runGit(repoRoot, ["fetch", "origin"]); // Explicitly set refs/remotes/origin/HEAD to exercise the symbolic-ref path // (git remote set-head -a requires the remote to advertise HEAD, so we set it manually) await runGit(repoRoot, ["remote", "set-head", "origin", "master"]); const { recorder, operations } = createWorkspaceOperationRecorderDouble(); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: null, }, config: { workspaceStrategy: { type: "git_worktree", // No baseRef configured — should auto-detect "master" via symbolic-ref }, }, issue: { id: "issue-1", identifier: "PAP-461", title: "Auto detect default branch via symref", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, recorder, }); expect(workspace.strategy).toBe("git_worktree"); expect(workspace.created).toBe(true); const worktreeOp = operations.find(op => op.phase === "worktree_prepare" && op.metadata?.created); expect(worktreeOp).toBeDefined(); expect(worktreeOp!.metadata!.baseRef).toBe("master"); }); it("removes a created git worktree and branch during cleanup", async () => { const repoRoot = await createTempRepo(); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-1", identifier: "PAP-449", title: "Cleanup workspace", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); const cleanup = await cleanupExecutionWorkspaceArtifacts({ workspace: { id: "execution-workspace-1", cwd: workspace.cwd, providerType: "git_worktree", providerRef: workspace.worktreePath, branchName: workspace.branchName, repoUrl: workspace.repoUrl, baseRef: workspace.repoRef, projectId: workspace.projectId, projectWorkspaceId: workspace.workspaceId, sourceIssueId: "issue-1", metadata: { createdByRuntime: true, }, }, projectWorkspace: { cwd: repoRoot, cleanupCommand: null, }, }); expect(cleanup.cleaned).toBe(true); expect(cleanup.warnings).toEqual([]); await expect(fs.stat(workspace.cwd)).rejects.toThrow(); await expect( execFileAsync("git", ["branch", "--list", workspace.branchName!], { cwd: repoRoot }), ).resolves.toMatchObject({ stdout: "", }); }); it("keeps an unmerged runtime-created branch and warns instead of force deleting it", async () => { const repoRoot = await createTempRepo(); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-1", identifier: "PAP-451", title: "Keep unmerged branch", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); await fs.writeFile(path.join(workspace.cwd, "unmerged.txt"), "still here\n", "utf8"); await runGit(workspace.cwd, ["add", "unmerged.txt"]); await runGit(workspace.cwd, ["commit", "-m", "Keep unmerged work"]); const cleanup = await cleanupExecutionWorkspaceArtifacts({ workspace: { id: "execution-workspace-1", cwd: workspace.cwd, providerType: "git_worktree", providerRef: workspace.worktreePath, branchName: workspace.branchName, repoUrl: workspace.repoUrl, baseRef: workspace.repoRef, projectId: workspace.projectId, projectWorkspaceId: workspace.workspaceId, sourceIssueId: "issue-1", metadata: { createdByRuntime: true, }, }, projectWorkspace: { cwd: repoRoot, cleanupCommand: null, }, }); expect(cleanup.cleaned).toBe(true); expect(cleanup.warnings).toHaveLength(1); expect(cleanup.warnings[0]).toContain(`Skipped deleting branch "${workspace.branchName}"`); await expect( execFileAsync("git", ["branch", "--list", workspace.branchName!], { cwd: repoRoot }), ).resolves.toMatchObject({ stdout: expect.stringContaining(workspace.branchName!), }); }); it("records teardown and cleanup operations when a recorder is provided", async () => { const repoRoot = await createTempRepo(); const { recorder, operations } = createWorkspaceOperationRecorderDouble(); const workspace = await realizeExecutionWorkspace({ base: { baseCwd: repoRoot, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: "HEAD", }, config: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}-{{slug}}", }, }, issue: { id: "issue-1", identifier: "PAP-541", title: "Cleanup recorder", }, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, }); await cleanupExecutionWorkspaceArtifacts({ workspace: { id: "execution-workspace-1", cwd: workspace.cwd, providerType: "git_worktree", providerRef: workspace.worktreePath, branchName: workspace.branchName, repoUrl: workspace.repoUrl, baseRef: workspace.repoRef, projectId: workspace.projectId, projectWorkspaceId: workspace.workspaceId, sourceIssueId: "issue-1", metadata: { createdByRuntime: true, }, }, projectWorkspace: { cwd: repoRoot, cleanupCommand: "printf 'cleanup ok\\n'", }, recorder, }); expect(operations.map((operation) => operation.phase)).toEqual([ "workspace_teardown", "worktree_cleanup", "worktree_cleanup", ]); expect(operations[0]?.command).toBe("printf 'cleanup ok\\n'"); expect(operations[1]?.metadata).toMatchObject({ cleanupAction: "worktree_remove", }); expect(operations[2]?.metadata).toMatchObject({ cleanupAction: "branch_delete", }); }); }); describe("ensureRuntimeServicesForRun", () => { it("reuses shared runtime services across runs and starts a new service after release", async () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-workspace-")); const workspace = buildWorkspace(workspaceRoot); const serviceCommand = "node -e \"require('node:http').createServer((req,res)=>res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1')\""; const config = { workspaceRuntime: { services: [ { name: "web", command: serviceCommand, port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, expose: { type: "url", urlTemplate: "http://127.0.0.1:{{port}}", }, lifecycle: "shared", reuseScope: "project_workspace", stopPolicy: { type: "on_run_finish", }, }, ], }, }; const run1 = "run-1"; const run2 = "run-2"; leasedRunIds.add(run1); leasedRunIds.add(run2); const first = await ensureRuntimeServicesForRun({ runId: run1, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace, config, adapterEnv: {}, }); expect(first).toHaveLength(1); expect(first[0]?.reused).toBe(false); expect(first[0]?.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); const response = await fetch(first[0]!.url!); expect(await response.text()).toBe("ok"); const second = await ensureRuntimeServicesForRun({ runId: run2, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace, config, adapterEnv: {}, }); expect(second).toHaveLength(1); expect(second[0]?.reused).toBe(true); expect(second[0]?.id).toBe(first[0]?.id); await releaseRuntimeServicesForRun(run1); leasedRunIds.delete(run1); await releaseRuntimeServicesForRun(run2); leasedRunIds.delete(run2); const run3 = "run-3"; leasedRunIds.add(run3); const third = await ensureRuntimeServicesForRun({ runId: run3, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace, config, adapterEnv: {}, }); expect(third).toHaveLength(1); expect(third[0]?.reused).toBe(false); expect(third[0]?.id).not.toBe(first[0]?.id); }); it("does not reuse project-scoped shared services across different workspace launch contexts", async () => { const primaryWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-primary-")); const worktreeWorkspaceRoot = path.join(primaryWorkspaceRoot, ".paperclip", "worktrees", "PAP-874-chat-speed-issues"); await fs.mkdir(worktreeWorkspaceRoot, { recursive: true }); const primaryWorkspace = buildWorkspace(primaryWorkspaceRoot); const executionWorkspace: RealizedExecutionWorkspace = { ...buildWorkspace(worktreeWorkspaceRoot), source: "task_session", strategy: "git_worktree", cwd: worktreeWorkspaceRoot, branchName: "PAP-874-chat-speed-issues", worktreePath: worktreeWorkspaceRoot, }; const serviceCommand = "node -e \"require('node:http').createServer((req,res)=>res.end(process.env.PAPERCLIP_HOME)).listen(Number(process.env.PORT), '127.0.0.1')\""; const config = { workspaceRuntime: { services: [ { name: "paperclip-dev", command: serviceCommand, cwd: ".", env: { PAPERCLIP_HOME: "{{workspace.cwd}}/.paperclip/runtime-services", }, port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, expose: { type: "url", urlTemplate: "http://127.0.0.1:{{port}}", }, lifecycle: "shared", reuseScope: "project_workspace", stopPolicy: { type: "on_run_finish", }, }, ], }, }; const primaryRunId = "run-project-workspace"; const executionRunId = "run-execution-workspace"; leasedRunIds.add(primaryRunId); leasedRunIds.add(executionRunId); const primaryServices = await ensureRuntimeServicesForRun({ runId: primaryRunId, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace: primaryWorkspace, config, adapterEnv: {}, }); const executionServices = await ensureRuntimeServicesForRun({ runId: executionRunId, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace: executionWorkspace, executionWorkspaceId: "execution-workspace-1", config, adapterEnv: {}, }); expect(primaryServices).toHaveLength(1); expect(executionServices).toHaveLength(1); expect(primaryServices[0]?.reused).toBe(false); expect(executionServices[0]?.reused).toBe(false); expect(executionServices[0]?.id).not.toBe(primaryServices[0]?.id); expect(executionServices[0]?.executionWorkspaceId).toBe("execution-workspace-1"); expect(executionServices[0]?.cwd).toBe(worktreeWorkspaceRoot); expect(executionServices[0]?.url).not.toBe(primaryServices[0]?.url); const primaryResponse = await fetch(primaryServices[0]!.url!); expect(await primaryResponse.text()).toBe(path.join(primaryWorkspaceRoot, ".paperclip", "runtime-services")); const executionResponse = await fetch(executionServices[0]!.url!); expect(await executionResponse.text()).toBe(path.join(worktreeWorkspaceRoot, ".paperclip", "runtime-services")); }); it("does not leak parent Paperclip instance env into runtime service commands", async () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-env-")); const workspace = buildWorkspace(workspaceRoot); const envCapturePath = path.join(workspaceRoot, "captured-env.json"); const serviceCommand = [ "node -e", JSON.stringify( [ "const fs = require('node:fs');", `fs.writeFileSync(${JSON.stringify(envCapturePath)}, JSON.stringify({`, "paperclipConfig: process.env.PAPERCLIP_CONFIG ?? null,", "paperclipHome: process.env.PAPERCLIP_HOME ?? null,", "paperclipInstanceId: process.env.PAPERCLIP_INSTANCE_ID ?? null,", "databaseUrl: process.env.DATABASE_URL ?? null,", "customEnv: process.env.RUNTIME_CUSTOM_ENV ?? null,", "port: process.env.PORT ?? null,", "}));", "require('node:http').createServer((req, res) => res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1');", ].join(" "), ), ].join(" "); process.env.PAPERCLIP_CONFIG = "/tmp/base-paperclip-config.json"; process.env.PAPERCLIP_HOME = "/tmp/base-paperclip-home"; process.env.PAPERCLIP_INSTANCE_ID = "base-instance"; process.env.DATABASE_URL = "postgres://shared-db.example.com/paperclip"; const runId = "run-env"; leasedRunIds.add(runId); const services = await ensureRuntimeServicesForRun({ runId, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace, executionWorkspaceId: "execution-workspace-1", config: { workspaceRuntime: { services: [ { name: "web", command: serviceCommand, port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, lifecycle: "shared", reuseScope: "execution_workspace", stopPolicy: { type: "on_run_finish", }, }, ], }, }, adapterEnv: { RUNTIME_CUSTOM_ENV: "from-adapter", }, }); expect(services).toHaveLength(1); const captured = JSON.parse(await fs.readFile(envCapturePath, "utf8")) as Record; expect(captured.paperclipConfig).toBeNull(); expect(captured.paperclipHome).toBeNull(); expect(captured.paperclipInstanceId).toBeNull(); expect(captured.databaseUrl).toBeNull(); expect(captured.customEnv).toBe("from-adapter"); expect(captured.port).toMatch(/^\d+$/); expect(services[0]?.executionWorkspaceId).toBe("execution-workspace-1"); expect(services[0]?.scopeType).toBe("execution_workspace"); expect(services[0]?.scopeId).toBe("execution-workspace-1"); }); it("stops execution workspace runtime services by executionWorkspaceId", async () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-stop-")); const workspace = buildWorkspace(workspaceRoot); const runId = "run-stop"; leasedRunIds.add(runId); const services = await ensureRuntimeServicesForRun({ runId, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace, executionWorkspaceId: "execution-workspace-stop", config: { workspaceRuntime: { services: [ { name: "web", command: "node -e \"require('node:http').createServer((req,res)=>res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1')\"", port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, lifecycle: "shared", reuseScope: "execution_workspace", stopPolicy: { type: "manual", }, }, ], }, }, adapterEnv: {}, }); expect(services[0]?.url).toBeTruthy(); await stopRuntimeServicesForExecutionWorkspace({ executionWorkspaceId: "execution-workspace-stop", workspaceCwd: workspace.cwd, }); await releaseRuntimeServicesForRun(runId); leasedRunIds.delete(runId); await new Promise((resolve) => setTimeout(resolve, 250)); await expect(fetch(services[0]!.url!)).rejects.toThrow(); }); it("does not stop services in sibling directories when matching by workspace cwd", async () => { const workspaceParent = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-sibling-")); const targetWorkspaceRoot = path.join(workspaceParent, "project"); const siblingWorkspaceRoot = path.join(workspaceParent, "project-extended", "service"); await fs.mkdir(targetWorkspaceRoot, { recursive: true }); await fs.mkdir(siblingWorkspaceRoot, { recursive: true }); const siblingWorkspace = buildWorkspace(siblingWorkspaceRoot); const runId = "run-sibling"; leasedRunIds.add(runId); const services = await ensureRuntimeServicesForRun({ runId, agent: { id: "agent-1", name: "Codex Coder", companyId: "company-1", }, issue: null, workspace: siblingWorkspace, executionWorkspaceId: "execution-workspace-sibling", config: { workspaceRuntime: { services: [ { name: "web", command: "node -e \"require('node:http').createServer((req,res)=>res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1')\"", port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, lifecycle: "shared", reuseScope: "execution_workspace", stopPolicy: { type: "manual", }, }, ], }, }, adapterEnv: {}, }); await stopRuntimeServicesForExecutionWorkspace({ executionWorkspaceId: "execution-workspace-target", workspaceCwd: targetWorkspaceRoot, }); const response = await fetch(services[0]!.url!); expect(await response.text()).toBe("ok"); await releaseRuntimeServicesForRun(runId); leasedRunIds.delete(runId); }); }); describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-workspace-runtime-"); db = createDb(tempDb.connectionString); }, 20_000); afterAll(async () => { await tempDb?.cleanup(); }); afterEach(async () => { await db.delete(workspaceRuntimeServices); await db.delete(executionWorkspaces); await db.delete(projects); await db.delete(heartbeatRuns); await db.delete(agents); await db.delete(companies); }); it("adopts a live auto-port shared service after runtime state is reset", async () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-reconcile-")); const paperclipHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-home-")); process.env.PAPERCLIP_HOME = paperclipHome; process.env.PAPERCLIP_INSTANCE_ID = `runtime-reconcile-${randomUUID()}`; const companyId = randomUUID(); const agentId = randomUUID(); const runId = randomUUID(); const executionWorkspaceId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values({ id: agentId, companyId, name: "Codex Coder", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, invocationSource: "manual", status: "running", startedAt: new Date(), updatedAt: new Date(), }); const workspace = { ...buildWorkspace(workspaceRoot), projectId: null, workspaceId: null, }; leasedRunIds.add(runId); const services = await ensureRuntimeServicesForRun({ db, runId, agent: { id: agentId, name: "Codex Coder", companyId, }, issue: null, workspace, config: { workspaceRuntime: { services: [ { name: "web", command: "node -e \"require('node:http').createServer((req,res)=>res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1')\"", port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, lifecycle: "shared", reuseScope: "agent", stopPolicy: { type: "manual", }, }, ], }, }, adapterEnv: {}, }); expect(services).toHaveLength(1); const service = services[0]; expect(service?.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); await expect(fetch(service!.url!)).resolves.toMatchObject({ ok: true }); await resetRuntimeServicesForTests(); const result = await reconcilePersistedRuntimeServicesOnStartup(db); expect(result).toMatchObject({ reconciled: 1, adopted: 1, stopped: 0 }); const persisted = await db .select() .from(workspaceRuntimeServices) .where(eq(workspaceRuntimeServices.id, service!.id)) .then((rows) => rows[0] ?? null); expect(persisted?.status).toBe("running"); expect(persisted?.providerRef).toMatch(/^\d+$/); await stopRuntimeServicesForExecutionWorkspace({ db, executionWorkspaceId, workspaceCwd: workspace.cwd, }); await expect(fetch(service!.url!)).rejects.toThrow(); }); it("persists controlled execution workspace stops as stopped", async () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-stop-persisted-")); const companyId = randomUUID(); const agentId = randomUUID(); const projectId = randomUUID(); const runId = randomUUID(); const executionWorkspaceId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values({ id: agentId, companyId, name: "Codex Coder", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(projects).values({ id: projectId, companyId, name: "Runtime stop test", status: "active", }); await db.insert(executionWorkspaces).values({ id: executionWorkspaceId, companyId, projectId, mode: "isolated_workspace", strategyType: "git_worktree", name: "Execution workspace stop test", status: "active", cwd: workspaceRoot, providerType: "local_fs", providerRef: workspaceRoot, }); await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, invocationSource: "manual", status: "running", startedAt: new Date(), updatedAt: new Date(), }); const workspace = { ...buildWorkspace(workspaceRoot), projectId: null, workspaceId: null, }; leasedRunIds.add(runId); const services = await ensureRuntimeServicesForRun({ db, runId, agent: { id: agentId, name: "Codex Coder", companyId, }, issue: null, workspace, executionWorkspaceId, config: { workspaceRuntime: { services: [ { name: "web", command: "node -e \"require('node:http').createServer((req,res)=>res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1')\"", port: { type: "auto" }, readiness: { type: "http", urlTemplate: "http://127.0.0.1:{{port}}", timeoutSec: 10, intervalMs: 100, }, lifecycle: "shared", reuseScope: "execution_workspace", stopPolicy: { type: "manual", }, }, ], }, }, adapterEnv: {}, }); expect(services[0]?.url).toBeTruthy(); await stopRuntimeServicesForExecutionWorkspace({ db, executionWorkspaceId, workspaceCwd: workspace.cwd, }); await releaseRuntimeServicesForRun(runId); leasedRunIds.delete(runId); await new Promise((resolve) => setTimeout(resolve, 250)); await expect(fetch(services[0]!.url!)).rejects.toThrow(); const persisted = await db .select() .from(workspaceRuntimeServices) .where(eq(workspaceRuntimeServices.id, services[0]!.id)) .then((rows) => rows[0] ?? null); expect(persisted?.status).toBe("stopped"); expect(persisted?.healthStatus).toBe("unknown"); expect(persisted?.stoppedAt).toBeTruthy(); }); }); describe("normalizeAdapterManagedRuntimeServices", () => { it("fills workspace defaults and derives stable ids for adapter-managed services", () => { const workspace = buildWorkspace("/tmp/project"); const now = new Date("2026-03-09T12:00:00.000Z"); const first = normalizeAdapterManagedRuntimeServices({ adapterType: "openclaw_gateway", runId: "run-1", agent: { id: "agent-1", name: "Gateway Agent", companyId: "company-1", }, issue: { id: "issue-1", identifier: "PAP-447", title: "Worktree support", }, workspace, reports: [ { serviceName: "preview", url: "https://preview.example/run-1", providerRef: "sandbox-123", scopeType: "run", }, ], now, }); const second = normalizeAdapterManagedRuntimeServices({ adapterType: "openclaw_gateway", runId: "run-1", agent: { id: "agent-1", name: "Gateway Agent", companyId: "company-1", }, issue: { id: "issue-1", identifier: "PAP-447", title: "Worktree support", }, workspace, reports: [ { serviceName: "preview", url: "https://preview.example/run-1", providerRef: "sandbox-123", scopeType: "run", }, ], now, }); expect(first).toHaveLength(1); expect(first[0]).toMatchObject({ companyId: "company-1", projectId: "project-1", projectWorkspaceId: "workspace-1", executionWorkspaceId: null, issueId: "issue-1", serviceName: "preview", provider: "adapter_managed", status: "running", healthStatus: "healthy", startedByRunId: "run-1", }); expect(first[0]?.id).toBe(second[0]?.id); }); it("prefers execution workspace ids over cwd for execution-scoped adapter services", () => { const workspace = buildWorkspace("/tmp/project"); const refs = normalizeAdapterManagedRuntimeServices({ adapterType: "openclaw_gateway", runId: "run-1", agent: { id: "agent-1", name: "Gateway Agent", companyId: "company-1", }, issue: null, workspace, executionWorkspaceId: "execution-workspace-1", reports: [ { serviceName: "preview", scopeType: "execution_workspace", }, ], }); expect(refs[0]).toMatchObject({ scopeType: "execution_workspace", scopeId: "execution-workspace-1", executionWorkspaceId: "execution-workspace-1", }); }); });