import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { createServer } from "node:net"; import { eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest"; import { agents, authAccounts, authUsers, companies, companyMemberships, createDb, closeRegisteredClients, ensurePostgresDatabase, executionWorkspaces, inspectMigrations, issueComments, issues, projectWorkspaces, instanceUserRoles, projects, routines, routineTriggers, workspaceRuntimeServices, } from "@paperclipai/db"; import { copyGitHooksToWorktreeGitDir, copySeededSecretsKey, ensureEmbeddedPostgres, ensureWorktreeSeeded, formatWorktreeSeedFailureDiagnostic, inspectLegacyWorktreeDatabase, markWorktreeSeedPending, pauseSeededScheduledRoutines, quarantineSeededWorktreeExecutionState, readWorktreeSeedManifest, readSourceAttachmentBody, rebindWorkspaceCwd, requiresWorktreeSeedCredentialAccount, resolveSourceConfigPath, resolveWorktreeReseedSource, resolveWorktreeReseedTargetPaths, resolveGitWorktreeAddArgs, resolvePnpmInstallInvocation, resolveCurrentWorktreeEndpoint, resolveWorktreeSeedMigrationRevision, resolveWorktreeSeedBackupEngine, resolveWorktreeMakeTargetPath, worktreeRepairCommand, worktreeInitCommand, worktreeMakeCommand, worktreeReseedCommand, } from "../commands/worktree.js"; import { buildWorktreeConfig, buildWorktreeEnvEntries, formatShellExports, generateWorktreeColor, resolveWorktreeSeedPlan, resolveWorktreeLocalPaths, rewriteLocalUrlPort, sanitizeWorktreeInstanceId, } from "../commands/worktree-lib.js"; import type { PaperclipConfig } from "../config/schema.js"; import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; const ORIGINAL_CWD = process.cwd(); const ORIGINAL_ENV = { ...process.env }; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); // Every test in the embedded-Postgres cost class shares one time budget // (see EMBEDDED_POSTGRES_TEST_TIMEOUT_MS) instead of a hand-written number // per call site. function itEmbeddedPostgres(name: string, fn: () => Promise): void { if (!embeddedPostgresSupport.supported) { it.skip(name, fn); return; } it(name, fn, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS); } const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; function mockVerifiedSeedResult() { return { backupSummary: "snapshot.sql", snapshotAt: "2026-08-18T00:00:00.000Z", migrationRevision: "0142_test.sql", pausedScheduledRoutines: 0, executionQuarantine: { disabledTimerHeartbeats: 0, resetRunningAgents: 0, quarantinedInProgressIssues: 0, unassignedTodoIssues: 0, unassignedReviewIssues: 0, stoppedProjectWorkspaceRuntimes: 0, stoppedExecutionWorkspaceRuntimes: 0, stoppedRuntimeServices: 0, }, reboundWorkspaces: [], validation: { authUserCount: 1, credentialAccountCount: 1, instanceAdminCount: 1, activeMembershipCount: 1, companyCount: 1, issueCount: 1, representativeCompanyId: "00000000-0000-4000-8000-000000000001", representativeIssueId: "00000000-0000-4000-8000-000000000002", migrationRevision: "0142_test.sql", }, }; } async function seedValidWorktreeSource( connectionString: string, options: { includeCredentialAccount?: boolean; userId?: string } = {}, ) { const db = createDb(connectionString); const companyId = randomUUID(); const issueId = randomUUID(); const userId = options.userId ?? "user-existing"; const now = new Date(); // This fixture also seeds a database before the latest migration. Use the // historical columns explicitly so current ORM defaults do not require new // columns that the worktree migration is supposed to add. await db.$client` INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) VALUES ( ${userId}, ${userId === "local-board" ? "Board" : "Existing User"}, ${userId === "local-board" ? "local@paperclip.local" : "existing@paperclip.ing"}, true, ${now.toISOString()}, ${now.toISOString()} ) `; if (options.includeCredentialAccount !== false) { await db.insert(authAccounts).values({ id: "credential-existing", // The issuer Better Auth stamps on an email/password account. issuer: "local:credential", accountId: "existing@paperclip.ing", providerId: "credential", userId, password: "fixture-password-hash", createdAt: now, updatedAt: now, }); } await db.insert(instanceUserRoles).values({ userId, role: "instance_admin", }); // Seed only columns present in the historical source schema. The current // model includes accounting columns that the worktree migration adds later. await db.$client` insert into companies (id, name, issue_prefix, require_board_approval_for_new_agents) values (${companyId}, 'Seed Source', 'SEED', false) `; await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, status: "active", }); // This helper also seeds an intentionally older schema. Current Drizzle // insert builders include defaults for newly added columns absent there. await db.$client` insert into issues (id, company_id, title, status, priority, issue_number, identifier) values (${issueId}, ${companyId}, 'Representative seed issue', 'backlog', 'medium', 1, 'SEED-1') `; await db.$client.end({ timeout: 5 }); return { companyId, issueId }; } if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres worktree CLI tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } async function reserveTestPort(): Promise<{ port: number; release: () => Promise }> { const server = createServer(); server.unref(); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); }); }); const address = server.address(); if (!address || typeof address === "string") { server.close(); throw new Error("Failed to reserve test port"); } let released = false; return { port: address.port, release: () => new Promise((resolve, reject) => { if (released) { resolve(); return; } released = true; server.close((error) => (error ? reject(error) : resolve())); }), }; } afterEach(() => { process.chdir(ORIGINAL_CWD); for (const key of Object.keys(process.env)) { if (!(key in ORIGINAL_ENV)) delete process.env[key]; } for (const [key, value] of Object.entries(ORIGINAL_ENV)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }); function buildSourceConfig(): PaperclipConfig { return { $meta: { version: 1, updatedAt: "2026-03-09T00:00:00.000Z", source: "configure", }, database: { mode: "embedded-postgres", embeddedPostgresDataDir: "/tmp/main/db", embeddedPostgresPort: 54329, backup: { enabled: true, intervalMinutes: 60, retentionDays: 30, dir: "/tmp/main/backups", }, }, logging: { mode: "file", logDir: "/tmp/main/logs", }, server: { deploymentMode: "authenticated", exposure: "private", host: "127.0.0.1", port: 3100, allowedHostnames: ["localhost"], serveUi: true, }, auth: { baseUrlMode: "explicit", publicBaseUrl: "http://127.0.0.1:3100", disableSignUp: false, }, telemetry: { enabled: true, }, storage: { provider: "local_disk", localDisk: { baseDir: "/tmp/main/storage", }, s3: { bucket: "paperclip", region: "us-east-1", prefix: "", forcePathStyle: false, }, }, secrets: { provider: "local_encrypted", strictMode: false, localEncrypted: { keyFilePath: "/tmp/main/secrets/master.key", }, }, }; } describe("worktree helpers", () => { it("uses the repo-local config for the current worktree", () => { const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-current-worktree-")); try { const localConfig = path.join(targetRoot, ".paperclip", "config.json"); fs.mkdirSync(path.dirname(localConfig), { recursive: true }); fs.writeFileSync(localConfig, "{}\n"); process.env.PAPERCLIP_CONFIG = "/tmp/ambient-paperclip/config.json"; process.chdir(targetRoot); expect(resolveCurrentWorktreeEndpoint()).toMatchObject({ rootPath: targetRoot, configPath: localConfig, isCurrent: true, }); } finally { process.chdir(ORIGINAL_CWD); fs.rmSync(targetRoot, { recursive: true, force: true }); } }); it("uses the repository config from a nested working directory", () => { const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-current-worktree-nested-")); try { execFileSync("git", ["init", "-q"], { cwd: targetRoot }); const nestedDirectory = path.join(targetRoot, "packages", "example", "src"); const localConfig = path.join(targetRoot, ".paperclip", "config.json"); fs.mkdirSync(nestedDirectory, { recursive: true }); fs.mkdirSync(path.dirname(localConfig), { recursive: true }); fs.writeFileSync(localConfig, "{}\n"); process.env.PAPERCLIP_CONFIG = "/tmp/ambient-paperclip/config.json"; process.chdir(nestedDirectory); expect(resolveCurrentWorktreeEndpoint()).toMatchObject({ rootPath: targetRoot, configPath: localConfig, isCurrent: true, }); } finally { process.chdir(ORIGINAL_CWD); fs.rmSync(targetRoot, { recursive: true, force: true }); } }); it("sanitizes instance ids", () => { expect(sanitizeWorktreeInstanceId("feature/worktree-support")).toBe("feature-worktree-support"); expect(sanitizeWorktreeInstanceId(" ")).toBe("worktree"); }); it("resolves worktree:make target paths under the user home directory", () => { expect(resolveWorktreeMakeTargetPath("paperclip-pr-432")).toBe( path.resolve(os.homedir(), "paperclip-pr-432"), ); }); it("rejects worktree:make names that are not safe directory/branch names", () => { expect(() => resolveWorktreeMakeTargetPath("paperclip/pr-432")).toThrow( "Worktree name must contain only letters, numbers, dots, underscores, or dashes.", ); }); it("reuses the current pnpm executable for worktree dependency installation", () => { expect( resolvePnpmInstallInvocation( { npm_execpath: "/Users/test/.pnpm/pnpm/9.15.4/bin/pnpm.cjs" }, "/usr/local/bin/node", ), ).toEqual({ command: "/usr/local/bin/node", argsPrefix: ["/Users/test/.pnpm/pnpm/9.15.4/bin/pnpm.cjs"], }); expect( resolvePnpmInstallInvocation( { npm_execpath: "/Users/test/.pnpm/pnpm/9.15.4/bin/pnpm" }, "/usr/local/bin/node", ), ).toEqual({ command: "/Users/test/.pnpm/pnpm/9.15.4/bin/pnpm", argsPrefix: [], }); expect( resolvePnpmInstallInvocation( { npm_execpath: "/Users/test/.npm/npm-cli.js" }, "/usr/local/bin/node", ), ).toEqual({ command: "pnpm", argsPrefix: [], }); }); it("builds git worktree add args for new and existing branches", () => { expect( resolveGitWorktreeAddArgs({ branchName: "feature-branch", targetPath: "/tmp/feature-branch", branchExists: false, }), ).toEqual(["worktree", "add", "-b", "feature-branch", "/tmp/feature-branch", "HEAD"]); expect( resolveGitWorktreeAddArgs({ branchName: "feature-branch", targetPath: "/tmp/feature-branch", branchExists: true, }), ).toEqual(["worktree", "add", "/tmp/feature-branch", "feature-branch"]); }); it("builds git worktree add args with a start point", () => { expect( resolveGitWorktreeAddArgs({ branchName: "my-worktree", targetPath: "/tmp/my-worktree", branchExists: false, startPoint: "public-gh/master", }), ).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "public-gh/master"]); }); it("uses start point even when a local branch with the same name exists", () => { expect( resolveGitWorktreeAddArgs({ branchName: "my-worktree", targetPath: "/tmp/my-worktree", branchExists: true, startPoint: "origin/main", }), ).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "origin/main"]); }); it("rewrites auth URLs only when they already include a port", () => { expect(rewriteLocalUrlPort("http://127.0.0.1:3100", 3110)).toBe("http://127.0.0.1:3110/"); expect(rewriteLocalUrlPort("http://my-host.ts.net:3100", 3110)).toBe("http://my-host.ts.net:3110/"); expect(rewriteLocalUrlPort("https://paperclip.example", 3110)).toBe("https://paperclip.example"); }); it("builds isolated config and env paths for a worktree", () => { const paths = resolveWorktreeLocalPaths({ cwd: "/tmp/paperclip-feature", homeDir: "/tmp/paperclip-worktrees", instanceId: "feature-worktree-support", }); const config = buildWorktreeConfig({ sourceConfig: buildSourceConfig(), paths, serverPort: 3110, databasePort: 54339, now: new Date("2026-03-09T12:00:00.000Z"), }); expect(config.database.embeddedPostgresDataDir).toBe( path.resolve("/tmp/paperclip-worktrees", "instances", "feature-worktree-support", "db"), ); expect(config.database.embeddedPostgresPort).toBe(54339); expect(config.database.backup.enabled).toBe(false); expect(config.server.port).toBe(3110); expect(config.auth.publicBaseUrl).toBe("http://127.0.0.1:3110/"); expect(config.storage.localDisk.baseDir).toBe( path.resolve("/tmp/paperclip-worktrees", "instances", "feature-worktree-support", "data", "storage"), ); const env = buildWorktreeEnvEntries(paths, { name: "feature-worktree-support", color: "#3abf7a", }); expect(env.PAPERCLIP_HOME).toBe(path.resolve("/tmp/paperclip-worktrees")); expect(env.PAPERCLIP_INSTANCE_ID).toBe("feature-worktree-support"); expect(env.PAPERCLIP_IN_WORKTREE).toBe("true"); expect(env.PAPERCLIP_DB_BACKUP_ENABLED).toBe("false"); expect(env.PAPERCLIP_WORKTREE_NAME).toBe("feature-worktree-support"); expect(env.PAPERCLIP_WORKTREE_COLOR).toBe("#3abf7a"); expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'"); }); it("falls back across storage roots before skipping a missing attachment object", async () => { const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" }); const expected = Buffer.from("image-bytes"); await expect( readSourceAttachmentBody( [ { getObject: vi.fn().mockRejectedValue(missingErr), }, { getObject: vi.fn().mockResolvedValue(expected), }, ], "company-1", "company-1/issues/issue-1/missing.png", ), ).resolves.toEqual(expected); }); it("returns null when an attachment object is missing from every lookup storage", async () => { const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" }); await expect( readSourceAttachmentBody( [ { getObject: vi.fn().mockRejectedValue(missingErr), }, { getObject: vi.fn().mockRejectedValue(Object.assign(new Error("missing"), { status: 404 })), }, ], "company-1", "company-1/issues/issue-1/missing.png", ), ).resolves.toBeNull(); }); it("generates vivid worktree colors as hex", () => { expect(generateWorktreeColor()).toMatch(/^#[0-9a-f]{6}$/); }); it("uses minimal seed mode to keep app state but drop heavy runtime history", () => { const minimal = resolveWorktreeSeedPlan("minimal"); const full = resolveWorktreeSeedPlan("full"); expect(minimal.excludedTables).toContain("heartbeat_runs"); expect(minimal.excludedTables).toContain("heartbeat_run_events"); expect(minimal.excludedTables).toContain("workspace_runtime_services"); expect(minimal.excludedTables).toContain("agent_task_sessions"); expect(minimal.nullifyColumns.issues).toEqual(["checkout_run_id", "execution_run_id"]); expect(full.excludedTables).toEqual(["agent_identity_keys"]); expect(minimal.excludedTables).toContain("agent_identity_keys"); expect(full.nullifyColumns).toEqual({}); }); it("requires the seed process to own the target embedded Postgres lifecycle", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-live-target-")); try { fs.writeFileSync( path.join(tempRoot, "postmaster.pid"), `${process.pid}\n${tempRoot}\n0\n55432\n`, ); await expect(ensureEmbeddedPostgres(tempRoot, 55432, { allowExisting: false })) .rejects.toThrow("while it is already running"); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("surfaces a credential-safe diagnostic when the target shuts down during restore", () => { expect(formatWorktreeSeedFailureDiagnostic( "restore", new Error( "Failed to restore seed.sql.gz: FATAL: the database system is shutting down; psql error: write EPIPE", ), )).toBe( "Target embedded PostgreSQL shut down during restore. Stop any competing worktree service and retry the seed.", ); expect(formatWorktreeSeedFailureDiagnostic("migrations", new Error("secret connection failure"))) .toBe("Seed failed during migrations."); }); it("surfaces the missing credential artifact for authenticated seed validation", () => { expect(formatWorktreeSeedFailureDiagnostic( "source_validation", new Error( "No auth user has a non-empty credential account, instance-admin role, and active company membership. Authenticated worktree seeding requires a credential-backed instance administrator.", ), )).toBe( "Seed validation could not find a credential-backed instance administrator with an active company membership. Authenticated instances must create or sign in an administrator before seeding.", ); }); it("requires credential accounts only for authenticated worktree seeds", () => { expect(requiresWorktreeSeedCredentialAccount("local_trusted")).toBe(false); expect(requiresWorktreeSeedCredentialAccount("authenticated")).toBe(true); }); it("rejects a source migration journal that diverges from the code journal", () => { expect(() => resolveWorktreeSeedMigrationRevision({ status: "upToDate", tableCount: 1, availableMigrations: ["0001_initial.sql", "0002_current.sql"], appliedMigrations: ["0001_initial.sql", "0003_unknown.sql"], journalEntryCount: 3, }, "sourcePrefix")).toThrow("Migration journal is not a prefix of this Paperclip checkout"); }); it("accepts a current source whose migration application order differs from filename order", () => { expect(resolveWorktreeSeedMigrationRevision({ status: "upToDate", tableCount: 1, availableMigrations: [ "0001_initial.sql", "0002_renumbered.sql", "0003_applied_earlier.sql", "0004_current.sql", ], appliedMigrations: [ "0001_initial.sql", "0003_applied_earlier.sql", "0002_renumbered.sql", "0004_current.sql", ], journalEntryCount: 6, }, "upToDate")).toBe("0004_current.sql"); }); it("accepts a source migration journal that is multiple revisions behind", () => { expect(resolveWorktreeSeedMigrationRevision({ status: "needsMigrations", tableCount: 1, availableMigrations: [ "0001_initial.sql", "0002_applied.sql", "0003_pending.sql", "0004_pending.sql", ], appliedMigrations: ["0002_applied.sql", "0001_initial.sql"], pendingMigrations: ["0003_pending.sql", "0004_pending.sql"], journalEntryCount: 3, reason: "pending-migrations", }, "sourcePrefix")).toBe("0002_applied.sql"); }); itEmbeddedPostgres("recognizes positive legacy database schema evidence", async () => { const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-legacy-evidence-"); onTestFinished(() => tempDb.cleanup()); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-legacy-config-")); try { const configPath = path.join(tempRoot, "config.json"); const sourceConfig = buildSourceConfig(); const config: PaperclipConfig = { ...sourceConfig, database: { ...sourceConfig.database, mode: "postgres", connectionString: tempDb.connectionString, backup: { ...sourceConfig.database.backup, enabled: false, intervalMinutes: 60, retentionDays: 30, dir: path.join(tempRoot, "backups"), }, }, }; fs.writeFileSync(configPath, `${JSON.stringify(config)}\n`); fs.writeFileSync( path.join(tempRoot, ".env"), `PAPERCLIP_INSTANCE_ID=legacy-target\nDATABASE_URL=${JSON.stringify(tempDb.connectionString)}\n`, ); await expect(inspectLegacyWorktreeDatabase(configPath)).resolves.toEqual({ migrationRevision: expect.stringMatching(/\.sql$/), }); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("ensure-seeded seeds once and fast-exits on the verified manifest", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "ensure-seeded-test", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3199, databasePort: 54999, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(targetRoot, ".paperclip", ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, ); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const seedDatabase = vi.fn().mockResolvedValue({ ...mockVerifiedSeedResult(), pausedScheduledRoutines: 2, executionQuarantine: { disabledTimerHeartbeats: 1, resetRunningAgents: 1, quarantinedInProgressIssues: 1, unassignedTodoIssues: 1, unassignedReviewIssues: 1, stoppedProjectWorkspaceRuntimes: 0, stoppedExecutionWorkspaceRuntimes: 0, stoppedRuntimeServices: 0, }, }); await expect( ensureWorktreeSeeded({ config: targetConfigPath, fromConfig: sourceConfigPath }, { seedDatabase }), ).resolves.toMatchObject({ seeded: true, reason: "seeded" }); await expect( ensureWorktreeSeeded({ config: targetConfigPath }, { seedDatabase }), ).resolves.toEqual({ seeded: false, reason: "verified_manifest" }); expect(seedDatabase).toHaveBeenCalledTimes(1); expect(seedDatabase).toHaveBeenCalledWith(expect.objectContaining({ sourceConfigPath, seedMode: "minimal", instanceId: "ensure-seeded-test", })); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed-pending"))).toBe(false); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed-complete"))).toBe(false); expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({ version: 2, state: "verified", phase: "complete", migrationRevision: "0142_test.sql", targetInstanceId: "ensure-seeded-test", }); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("treats an unregistered markerless config as a normal non-worktree boot", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unregistered-markerless-")); try { const configPath = path.join(tempRoot, "config.json"); fs.writeFileSync(configPath, `${JSON.stringify(buildSourceConfig())}\n`); delete process.env.PAPERCLIP_WORKSPACE_BASE_CWD; delete process.env.PAPERCLIP_PROJECT_WORKSPACE_ID; delete process.env.PAPERCLIP_SEED_EXPECTED_COMPANY_ID; const inspectLegacyDatabase = vi.fn(); const seedDatabase = vi.fn(); await expect(ensureWorktreeSeeded( { config: configPath }, { inspectLegacyDatabase, seedDatabase }, )).resolves.toEqual({ seeded: false, reason: "legacy_unmarked" }); expect(inspectLegacyDatabase).not.toHaveBeenCalled(); expect(seedDatabase).not.toHaveBeenCalled(); expect(readWorktreeSeedManifest(configPath)).toBeNull(); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("honors a legacy complete marker without resolving a seed source", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-complete-marker-")); try { const configPath = path.join(tempRoot, "config.json"); fs.writeFileSync(configPath, `${JSON.stringify(buildSourceConfig())}\n`); fs.writeFileSync(path.join(tempRoot, "seed-complete"), "complete\n"); delete process.env.PAPERCLIP_WORKSPACE_BASE_CWD; const inspectLegacyDatabase = vi.fn(); const seedDatabase = vi.fn(); await expect(ensureWorktreeSeeded( { config: configPath }, { inspectLegacyDatabase, seedDatabase }, )).resolves.toEqual({ seeded: false, reason: "complete_marker" }); expect(inspectLegacyDatabase).not.toHaveBeenCalled(); expect(seedDatabase).not.toHaveBeenCalled(); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("seeds a configured worktree with no seed markers when no legacy database is present", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unmarked-empty-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "unmarked-empty-target", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3194, databasePort: 54994, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(targetRoot, ".paperclip", ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, ); const inspectLegacyDatabase = vi.fn().mockResolvedValue(null); const seedDatabase = vi.fn().mockResolvedValue(mockVerifiedSeedResult()); await expect(ensureWorktreeSeeded( { config: targetConfigPath, fromConfig: sourceConfigPath }, { inspectLegacyDatabase, seedDatabase }, )).resolves.toMatchObject({ seeded: true, reason: "seeded" }); expect(inspectLegacyDatabase).toHaveBeenCalledWith(targetConfigPath); expect(seedDatabase).toHaveBeenCalledTimes(1); expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({ state: "verified", phase: "complete", migrationRevision: "0142_test.sql", }); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("adopts a markerless legacy worktree only after validating its database schema", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unmarked-legacy-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "unmarked-legacy-target", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3193, databasePort: 54993, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(targetRoot, ".paperclip", ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, ); const seedDatabase = vi.fn(); await expect(ensureWorktreeSeeded( { config: targetConfigPath, fromConfig: sourceConfigPath }, { inspectLegacyDatabase: vi.fn().mockResolvedValue({ migrationRevision: "0141_legacy.sql" }), seedDatabase, }, )).resolves.toEqual({ seeded: false, reason: "legacy_database" }); expect(seedDatabase).not.toHaveBeenCalled(); expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({ state: "verified", phase: "complete", migrationRevision: "0141_legacy.sql", }); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("managed ensure-seeded derives a valid source from the registered base workspace", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-managed-seed-")); try { const baseRoot = path.join(tempRoot, "base"); const sourceConfigPath = path.join(baseRoot, ".paperclip", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "managed-target", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3195, databasePort: 54995, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=managed-source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(path.dirname(targetConfigPath), ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=managed-target\n`, ); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const seedDatabase = vi.fn().mockResolvedValue(mockVerifiedSeedResult()); await expect(ensureWorktreeSeeded({ config: targetConfigPath, registeredBaseWorkspaceCwd: baseRoot, registeredProjectWorkspaceId: "project-workspace-1", expectedCompanyId: "company-1", }, { seedDatabase })).resolves.toMatchObject({ seeded: true, reason: "seeded" }); expect(seedDatabase).toHaveBeenCalledWith(expect.objectContaining({ sourceConfigPath, expectedCompanyId: "company-1", })); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it.each(["sibling", "foreign_instance", "symlink", "instance_mismatch"] as const)( "managed ensure-seeded re-derives a stale %s manifest source from registration", async (variant) => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `paperclip-worktree-managed-${variant}-`)); try { const baseRoot = path.join(tempRoot, "base"); const canonicalSource = path.join(baseRoot, ".paperclip", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const attackerRoot = path.join(tempRoot, variant); const attackerConfig = path.join(attackerRoot, "config.json"); fs.mkdirSync(path.dirname(canonicalSource), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.mkdirSync(attackerRoot, { recursive: true }); fs.writeFileSync(canonicalSource, `${JSON.stringify(buildSourceConfig())}\n`); fs.writeFileSync(path.join(path.dirname(canonicalSource), ".env"), "PAPERCLIP_INSTANCE_ID=registered-source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(buildSourceConfig())}\n`); fs.writeFileSync( path.join(path.dirname(targetConfigPath), ".env"), `PAPERCLIP_HOME=${path.join(tempRoot, "worktree-home")}\nPAPERCLIP_INSTANCE_ID=managed-target\n`, ); fs.writeFileSync(attackerConfig, `${JSON.stringify(buildSourceConfig())}\n`); fs.writeFileSync( path.join(attackerRoot, ".env"), `PAPERCLIP_INSTANCE_ID=${variant === "foreign_instance" ? "foreign" : "registered-source"}\n`, ); const diagnosticPath = variant === "instance_mismatch" ? canonicalSource : variant === "symlink" ? path.join(attackerRoot, "source-link.json") : attackerConfig; if (variant === "symlink") fs.symlinkSync(canonicalSource, diagnosticPath); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath: diagnosticPath, targetInstanceId: "managed-target", }); if (variant === "instance_mismatch") { const manifest = readWorktreeSeedManifest(targetConfigPath)!; fs.writeFileSync( path.join(path.dirname(targetConfigPath), "seed-manifest.json"), JSON.stringify({ ...manifest, source: { ...manifest.source, instanceId: "foreign" } }), ); } const seedDatabase = vi.fn().mockResolvedValue(mockVerifiedSeedResult()); await expect(ensureWorktreeSeeded({ config: targetConfigPath, registeredBaseWorkspaceCwd: baseRoot, registeredProjectWorkspaceId: "project-workspace-1", expectedCompanyId: "company-1", }, { seedDatabase })).resolves.toMatchObject({ seeded: true, reason: "seeded" }); expect(seedDatabase).toHaveBeenCalledWith(expect.objectContaining({ sourceConfigPath: canonicalSource, expectedCompanyId: "company-1", })); expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({ source: { configPath: canonicalSource, instanceId: "registered-source", }, state: "verified", diagnostics: expect.arrayContaining([ expect.objectContaining({ message: "Re-derived seed source diagnostics from the registered canonical source.", }), ]), }); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed.lock"))).toBe(false); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }, ); it("ensure-seeded records a target shutdown diagnostic when restore fails", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-failure-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "ensure-seeded-failure", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3198, databasePort: 54998, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(targetRoot, ".paperclip", ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, ); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); await expect( ensureWorktreeSeeded( { config: targetConfigPath, fromConfig: sourceConfigPath }, { seedDatabase: vi.fn(async (input) => { input.onPhase?.("restore", "started"); throw new Error( "Failed to restore seed.sql.gz: FATAL: the database system is shutting down; psql error: write EPIPE", ); }), }, ), ).rejects.toThrow("database system is shutting down"); expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({ state: "failed", phase: "restore", diagnostics: expect.arrayContaining([ expect.objectContaining({ phase: "restore", status: "failed", message: "Target embedded PostgreSQL shut down during restore. Stop any competing worktree service and retry the seed.", }), ]), }); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed-pending"))).toBe(false); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed-complete"))).toBe(false); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed.lock"))).toBe(false); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("serializes concurrent ensure-seeded calls across the seed marker lock", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-lock-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "ensure-seeded-lock", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3197, databasePort: 54997, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(targetRoot, ".paperclip", ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, ); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const seedDatabase = vi.fn(async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return mockVerifiedSeedResult(); }); const results = await Promise.all([ ensureWorktreeSeeded({ config: targetConfigPath, fromConfig: sourceConfigPath }, { seedDatabase }), ensureWorktreeSeeded({ config: targetConfigPath, fromConfig: sourceConfigPath }, { seedDatabase }), ]); expect(results).toEqual(expect.arrayContaining([ expect.objectContaining({ seeded: true, reason: "seeded" }), { seeded: false, reason: "verified_manifest" }, ])); expect(seedDatabase).toHaveBeenCalledTimes(1); expect(fs.existsSync(path.join(targetRoot, ".paperclip", "seed.lock"))).toBe(false); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("records an interrupted phase before retrying to a verified terminal state", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-interrupted-seed-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); const targetConfigPath = path.join(targetRoot, ".paperclip", "config.json"); const targetPaths = resolveWorktreeLocalPaths({ cwd: targetRoot, homeDir: path.join(tempRoot, "worktree-home"), instanceId: "interrupted-seed", }); const sourceConfig = buildSourceConfig(); const targetConfig = buildWorktreeConfig({ sourceConfig, paths: targetPaths, serverPort: 3196, databasePort: 54996, }); fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); fs.writeFileSync( path.join(targetRoot, ".paperclip", ".env"), `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, ); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const interrupted = readWorktreeSeedManifest(targetConfigPath)!; fs.writeFileSync( path.join(targetRoot, ".paperclip", "seed-manifest.json"), `${JSON.stringify({ ...interrupted, state: "running", phase: "restore" }, null, 2)}\n`, ); await expect(ensureWorktreeSeeded( { config: targetConfigPath, fromConfig: sourceConfigPath }, { seedDatabase: vi.fn().mockResolvedValue(mockVerifiedSeedResult()) }, )).resolves.toMatchObject({ seeded: true, reason: "seeded" }); const verified = readWorktreeSeedManifest(targetConfigPath)!; expect(verified.state).toBe("verified"); expect(verified.diagnostics).toEqual(expect.arrayContaining([ expect.objectContaining({ phase: "restore", status: "failed", message: "The previous seed attempt ended without a terminal result.", }), ])); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("fails closed instead of racing to reclaim a stale seed lock", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-stale-lock-")); try { const targetConfigPath = path.join(tempRoot, ".paperclip", "config.json"); const lockPath = path.join(tempRoot, ".paperclip", "seed.lock"); fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); fs.writeFileSync( lockPath, `${JSON.stringify({ version: 1, pid: 2_147_483_647, token: "stale-owner", createdAt: new Date(0).toISOString(), })}\n`, ); const seedDatabase = vi.fn(); await expect( ensureWorktreeSeeded({ config: targetConfigPath }, { seedDatabase }), ).rejects.toThrow("belongs to exited process"); expect(seedDatabase).not.toHaveBeenCalled(); expect(fs.existsSync(lockPath)).toBe(true); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); itEmbeddedPostgres("quarantines copied live execution state in seeded worktree databases", async () => { const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-quarantine-"); onTestFinished(() => tempDb.cleanup()); const db = createDb(tempDb.connectionString); const companyId = randomUUID(); const agentId = randomUUID(); const idleAgentId = randomUUID(); const inProgressIssueId = randomUUID(); const todoIssueId = randomUUID(); const reviewIssueId = randomUUID(); const userIssueId = randomUUID(); const projectId = randomUUID(); const projectWorkspaceId = randomUUID(); const executionWorkspaceId = randomUUID(); const runtimeServiceId = randomUUID(); try { await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: "WTQ", requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values([ { id: agentId, companyId, name: "CodexCoder", role: "engineer", status: "running", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: { heartbeat: { enabled: true, intervalSec: 60 }, wakeOnDemand: true, }, permissions: {}, }, { id: idleAgentId, companyId, name: "Reviewer", role: "reviewer", status: "idle", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: { heartbeat: { enabled: false, intervalSec: 300 } }, permissions: {}, }, ]); await db.insert(projects).values({ id: projectId, companyId, name: "Runtime quarantine", status: "in_progress", }); await db.insert(projectWorkspaces).values({ id: projectWorkspaceId, companyId, projectId, name: "Primary workspace", cwd: "/source/project", metadata: { keep: "project-metadata", runtimeConfig: { workspaceRuntime: { services: [{ name: "paperclip-dev" }] }, desiredState: "running", serviceStates: { "0": "running", "1": "manual" }, }, }, }); await db.insert(executionWorkspaces).values({ id: executionWorkspaceId, companyId, projectId, projectWorkspaceId, mode: "isolated_workspace", strategyType: "git_worktree", name: "Copied runtime workspace", cwd: "/source/worktree", providerType: "git_worktree", metadata: { keep: "execution-metadata", config: { environmentId: "environment-1", desiredState: "running", serviceStates: { "0": "running" }, }, }, }); await db.insert(workspaceRuntimeServices).values({ id: runtimeServiceId, companyId, projectId, projectWorkspaceId, executionWorkspaceId, scopeType: "project_workspace", scopeId: projectWorkspaceId, serviceName: "paperclip-dev", status: "running", lifecycle: "shared", provider: "local_process", providerRef: "12345", ownerAgentId: agentId, port: 42013, url: "https://paperclip-dev.example.test:42013", healthStatus: "healthy", }); await db.insert(issues).values([ { id: inProgressIssueId, companyId, title: "Copied in-flight issue", status: "in_progress", priority: "medium", assigneeAgentId: agentId, issueNumber: 1, identifier: "WTQ-1", executionAgentNameKey: "codexcoder", executionLockedAt: new Date("2026-04-18T00:00:00.000Z"), }, { id: todoIssueId, companyId, title: "Copied assigned todo issue", status: "todo", priority: "medium", assigneeAgentId: agentId, issueNumber: 2, identifier: "WTQ-2", }, { id: reviewIssueId, companyId, title: "Copied assigned review issue", status: "in_review", priority: "medium", assigneeAgentId: idleAgentId, issueNumber: 3, identifier: "WTQ-3", }, { id: userIssueId, companyId, title: "Copied user issue", status: "todo", priority: "medium", assigneeUserId: "user-1", issueNumber: 4, identifier: "WTQ-4", }, ]); await expect(quarantineSeededWorktreeExecutionState(tempDb.connectionString)).resolves.toEqual({ disabledTimerHeartbeats: 1, resetRunningAgents: 1, quarantinedInProgressIssues: 1, unassignedTodoIssues: 1, unassignedReviewIssues: 1, stoppedProjectWorkspaceRuntimes: 1, stoppedExecutionWorkspaceRuntimes: 1, stoppedRuntimeServices: 1, }); const [quarantinedAgent] = await db.select().from(agents).where(eq(agents.id, agentId)); expect(quarantinedAgent?.status).toBe("idle"); expect(quarantinedAgent?.runtimeConfig).toMatchObject({ heartbeat: { enabled: false, intervalSec: 60 }, wakeOnDemand: true, }); const [inProgressIssue] = await db.select().from(issues).where(eq(issues.id, inProgressIssueId)); expect(inProgressIssue?.status).toBe("blocked"); expect(inProgressIssue?.assigneeAgentId).toBeNull(); expect(inProgressIssue?.executionAgentNameKey).toBeNull(); expect(inProgressIssue?.executionLockedAt).toBeNull(); const [todoIssue] = await db.select().from(issues).where(eq(issues.id, todoIssueId)); expect(todoIssue?.status).toBe("todo"); expect(todoIssue?.assigneeAgentId).toBeNull(); const [reviewIssue] = await db.select().from(issues).where(eq(issues.id, reviewIssueId)); expect(reviewIssue?.status).toBe("in_review"); expect(reviewIssue?.assigneeAgentId).toBeNull(); const [userIssue] = await db.select().from(issues).where(eq(issues.id, userIssueId)); expect(userIssue?.status).toBe("todo"); expect(userIssue?.assigneeUserId).toBe("user-1"); const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, inProgressIssueId)); expect(comments).toHaveLength(1); expect(comments[0]?.body).toContain("Quarantined during worktree seed"); const [projectWorkspace] = await db .select() .from(projectWorkspaces) .where(eq(projectWorkspaces.id, projectWorkspaceId)); expect(projectWorkspace?.metadata).toEqual({ keep: "project-metadata", runtimeConfig: { workspaceRuntime: { services: [{ name: "paperclip-dev" }] }, desiredState: "stopped", serviceStates: { "0": "stopped", "1": "manual" }, }, }); const [executionWorkspace] = await db .select() .from(executionWorkspaces) .where(eq(executionWorkspaces.id, executionWorkspaceId)); expect(executionWorkspace?.metadata).toMatchObject({ keep: "execution-metadata", config: { environmentId: "environment-1", desiredState: "stopped", serviceStates: { "0": "stopped" }, }, }); const [runtimeService] = await db .select() .from(workspaceRuntimeServices) .where(eq(workspaceRuntimeServices.id, runtimeServiceId)); expect(runtimeService).toMatchObject({ status: "stopped", healthStatus: "unknown", providerRef: null, ownerAgentId: null, startedByRunId: null, port: null, url: null, }); expect(runtimeService?.stoppedAt).toBeInstanceOf(Date); } finally { await db.$client?.end?.({ timeout: 5 }).catch(() => undefined); } }); 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"); fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true }); fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8"); const sourceConfig = buildSourceConfig(); sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath; copySeededSecretsKey({ sourceConfigPath, sourceConfig, sourceEnvEntries: {}, targetKeyFilePath: targetKeyPath, }); 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 }); } }); it("writes the source inline secrets master key into the seeded worktree instance", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key"); copySeededSecretsKey({ sourceConfigPath, sourceConfig: buildSourceConfig(), sourceEnvEntries: { PAPERCLIP_SECRETS_MASTER_KEY: "inline-source-master-key", }, targetKeyFilePath: targetKeyPath, }); expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("inline-source-master-key"); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("creates an explicitly empty worktree without inherited signing secrets or deferred copying", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-empty-")); const originalCwd = process.cwd(); const originalJwt = process.env.PAPERCLIP_AGENT_JWT_SECRET; const originalSigning = process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET; try { const repoRoot = path.join(tempRoot, "repo"); fs.mkdirSync(repoRoot, { recursive: true }); process.chdir(repoRoot); process.env.PAPERCLIP_AGENT_JWT_SECRET = "source-jwt-secret"; process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = "source-signing-secret"; await worktreeInitCommand({ empty: true, fromConfig: path.join(tempRoot, "missing.json"), home: path.join(tempRoot, "instances") }); const env = fs.readFileSync(path.join(repoRoot, ".paperclip/.env"), "utf8"); expect(env).not.toContain("source-jwt-secret"); expect(env).not.toContain("source-signing-secret"); expect(env).toContain("PAPERCLIP_AGENT_JWT_SECRET="); expect(fs.existsSync(path.join(repoRoot, ".paperclip/seed-manifest.json"))).toBe(false); expect(fs.existsSync(path.join(repoRoot, ".paperclip/seed-pending"))).toBe(false); expect(fs.existsSync(path.join(repoRoot, ".paperclip/seed-empty"))).toBe(true); await worktreeInitCommand({ seed: false, force: true, fromConfig: path.join(tempRoot, "missing.json"), home: path.join(tempRoot, "instances") }); expect(fs.existsSync(path.join(repoRoot, ".paperclip/seed-empty"))).toBe(false); expect(readWorktreeSeedManifest(path.join(repoRoot, ".paperclip/config.json"))?.state).toBe("pending"); } finally { process.chdir(originalCwd); if (originalJwt === undefined) delete process.env.PAPERCLIP_AGENT_JWT_SECRET; else process.env.PAPERCLIP_AGENT_JWT_SECRET = originalJwt; if (originalSigning === undefined) delete process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET; else process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = originalSigning; fs.rmSync(tempRoot, { recursive: true, force: true }); } }); 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; const originalToolActionSigningSecret = process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET; try { fs.mkdirSync(repoRoot, { recursive: true }); process.env.PAPERCLIP_AGENT_JWT_SECRET = "worktree-shared-secret"; process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = "worktree-tool-action-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"); const envContents = fs.readFileSync(envPath, "utf8"); expect(envContents).toContain("PAPERCLIP_AGENT_JWT_SECRET=worktree-shared-secret"); expect(envContents).toContain("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=worktree-tool-action-secret"); expect(envContents).toContain("PAPERCLIP_WORKTREE_NAME=repo"); expect(envContents).toMatch(/PAPERCLIP_WORKTREE_COLOR=\"#[0-9a-f]{6}\"/); } finally { process.chdir(originalCwd); if (originalJwtSecret === undefined) { delete process.env.PAPERCLIP_AGENT_JWT_SECRET; } else { process.env.PAPERCLIP_AGENT_JWT_SECRET = originalJwtSecret; } if (originalToolActionSigningSecret === undefined) { delete process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET; } else { process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = originalToolActionSigningSecret; } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("preserves repo-managed worktree checkouts when --force re-runs from the source repo", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-force-preserve-")); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); try { fs.mkdirSync(repoRoot, { recursive: true }); const repoConfigDir = path.join(repoRoot, ".paperclip"); fs.mkdirSync(repoConfigDir, { recursive: true }); fs.writeFileSync(path.join(repoConfigDir, "config.json"), "stale", "utf8"); fs.writeFileSync(path.join(repoConfigDir, ".env"), "STALE=1", "utf8"); // Simulate the repo-managed worktrees subfolder that holds every // worktree checkout (the directory PAPA-358 reported as nuked). const worktreesDir = path.join(repoConfigDir, "worktrees"); const checkoutDir = path.join(worktreesDir, "PAP-100-feature"); fs.mkdirSync(checkoutDir, { recursive: true }); const sentinelPath = path.join(checkoutDir, "sentinel.txt"); fs.writeFileSync(sentinelPath, "do-not-delete", "utf8"); process.chdir(repoRoot); await worktreeInitCommand({ seed: false, force: true, fromConfig: path.join(tempRoot, "missing", "config.json"), home: path.join(tempRoot, ".paperclip-worktrees"), }); expect(fs.existsSync(sentinelPath)).toBe(true); expect(fs.readFileSync(sentinelPath, "utf8")).toBe("do-not-delete"); expect(fs.existsSync(path.join(repoConfigDir, "config.json"))).toBe(true); expect(fs.readFileSync(path.join(repoConfigDir, "config.json"), "utf8")).not.toBe("stale"); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); } }); itEmbeddedPostgres( "seeds a local-trusted implicit board user without a credential account", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-local-board-seed-")); const originalCwd = process.cwd(); onTestFinished(() => { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); }); const worktreeRoot = path.join(tempRoot, "PAP-17696-local-board-seed"); const sourceConfigDir = path.join(tempRoot, "source"); const sourceConfigPath = path.join(sourceConfigDir, "config.json"); const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key"); const worktreeHome = path.join(tempRoot, ".paperclip-worktrees"); const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-local-board-source-"); onTestFinished(() => sourceDb.cleanup()); await seedValidWorktreeSource(sourceDb.connectionString, { includeCredentialAccount: false, userId: "local-board", }); fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true }); fs.mkdirSync(worktreeRoot, { recursive: true }); const sourceConfig = buildSourceConfig(); sourceConfig.database = { ...sourceConfig.database, mode: "postgres", connectionString: sourceDb.connectionString, }; sourceConfig.server.deploymentMode = "local_trusted"; sourceConfig.server.exposure = "private"; sourceConfig.auth.baseUrlMode = "auto"; delete sourceConfig.auth.publicBaseUrl; sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath; fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig, null, 2)}\n`, "utf8"); fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8"); process.chdir(worktreeRoot); await worktreeInitCommand({ name: "PAP-17696-local-board-seed", home: worktreeHome, fromConfig: sourceConfigPath, force: true, }); const targetConfigPath = path.join(worktreeRoot, ".paperclip", "config.json"); const targetConfig = JSON.parse(fs.readFileSync(targetConfigPath, "utf8")) as PaperclipConfig; expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({ state: "verified", phase: "complete", }); const { default: EmbeddedPostgres } = await import("embedded-postgres"); const targetPg = new EmbeddedPostgres({ databaseDir: targetConfig.database.embeddedPostgresDataDir, user: "paperclip", password: "paperclip", port: targetConfig.database.embeddedPostgresPort, persistent: true, initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"], onLog: () => {}, onError: () => {}, }); await targetPg.start(); onTestFinished(() => targetPg.stop()); const targetDb = createDb( `postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`, ); const [seededLocalBoard] = await targetDb .select({ id: authUsers.id }) .from(authUsers) .where(eq(authUsers.id, "local-board")); const seededAccounts = await targetDb.select().from(authAccounts); expect(seededLocalBoard?.id).toBe("local-board"); expect(seededAccounts).toHaveLength(0); await targetDb.$client.end({ timeout: 5 }); }, ); itEmbeddedPostgres( "seeds a lagging source whose migration application order differs from filename order", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-")); const originalCwd = process.cwd(); onTestFinished(() => { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); }); const worktreeRoot = path.join(tempRoot, "PAP-999-auth-seed"); const sourceHome = path.join(tempRoot, "source-home"); const sourceConfigDir = path.join(sourceHome, "instances", "source"); const sourceConfigPath = path.join(sourceConfigDir, "config.json"); const sourceEnvPath = path.join(sourceConfigDir, ".env"); const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key"); const worktreeHome = path.join(tempRoot, ".paperclip-worktrees"); const sourceCluster = await startEmbeddedPostgresTestDatabase("paperclip-worktree-auth-source-"); const sourceUrl = new URL(sourceCluster.connectionString); sourceUrl.pathname = "/lagging_source"; const sourceDb = { connectionString: sourceUrl.toString() }; onTestFinished(async () => { await closeRegisteredClients(sourceDb.connectionString); await sourceCluster.cleanup(); }); await ensurePostgresDatabase(sourceCluster.connectionString, "lagging_source"); // A lagging source must also have the prior schema. Deleting only the // newest receipt from a fully migrated schema relied on that particular // migration being idempotent and breaks when the new migration creates a // table. Build the schema before the identity-repair migration, so this // regression keeps testing that repair as later migrations are added. const migrationsRoot = new URL("../../../packages/db/src/migrations/", import.meta.url); const journal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", migrationsRoot), "utf8")); const repairIndex = journal.entries.findIndex((entry: { tag: string }) => entry.tag === "0309_loving_the_hood"); expect(repairIndex).toBeGreaterThan(0); const priorEntries = journal.entries.slice(0, repairIndex); const priorMigrations = path.join(tempRoot, "prior-migrations"); fs.mkdirSync(path.join(priorMigrations, "meta"), { recursive: true }); fs.writeFileSync(path.join(priorMigrations, "meta", "_journal.json"), JSON.stringify({ ...journal, entries: priorEntries })); for (const entry of priorEntries) { fs.copyFileSync(new URL(`${entry.tag}.sql`, migrationsRoot), path.join(priorMigrations, `${entry.tag}.sql`)); } const sourceDbClient = createDb(sourceDb.connectionString); await migrate(drizzle(sourceDbClient.$client), { migrationsFolder: priorMigrations }); const seed = await seedValidWorktreeSource(sourceDb.connectionString); // An older filtered JavaScript backup retained event IDs but lost the // identity generator. The pending migration must repair that schema. const legacyAgentId = randomUUID(); await sourceDbClient.$client` INSERT INTO agents (id, company_id, name, status) VALUES (${legacyAgentId}, ${seed.companyId}, 'Legacy paused agent', 'paused') `; await sourceDbClient.$client` INSERT INTO resource_lifecycle_events (company_id, resource_type, resource_id, action) VALUES (${seed.companyId}, 'agent', ${legacyAgentId}, 'pause') `; await sourceDbClient.$client.unsafe('ALTER TABLE resource_lifecycle_events ALTER COLUMN id DROP IDENTITY'); await sourceDbClient.$client.unsafe(` WITH pair AS ( SELECT array_agg("id" ORDER BY "id" DESC) AS ids, array_agg("hash" ORDER BY "id" DESC) AS hashes FROM ( SELECT "id", "hash" FROM "drizzle"."__drizzle_migrations" ORDER BY "id" DESC LIMIT 2 ) latest ) UPDATE "drizzle"."__drizzle_migrations" migrations SET "hash" = CASE WHEN migrations."id" = pair.ids[1] THEN pair.hashes[2] WHEN migrations."id" = pair.ids[2] THEN pair.hashes[1] ELSE migrations."hash" END FROM pair WHERE migrations."id" IN (pair.ids[1], pair.ids[2]); INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES ('stale-unresolvable-migration-hash', 0) `); await sourceDbClient.$client.end({ timeout: 5 }); const laggingMigrationState = await inspectMigrations(sourceDb.connectionString); expect(laggingMigrationState.status).toBe("needsMigrations"); if (laggingMigrationState.status !== "needsMigrations") { throw new Error("Expected the source migration journal to lag the code journal"); } expect(laggingMigrationState.pendingMigrations).toHaveLength(journal.entries.length - repairIndex); const expectedAppliedPrefix = laggingMigrationState.availableMigrations.slice( 0, laggingMigrationState.appliedMigrations.length, ); expect(laggingMigrationState.appliedMigrations).not.toEqual(expectedAppliedPrefix); expect([...laggingMigrationState.appliedMigrations].sort()).toEqual( [...expectedAppliedPrefix].sort(), ); expect(laggingMigrationState.journalEntryCount).toBeGreaterThan( laggingMigrationState.appliedMigrations.length, ); const sourceMigrationRevision = expectedAppliedPrefix.at(-1); expect(sourceMigrationRevision).toBeTruthy(); fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true }); fs.mkdirSync(worktreeRoot, { recursive: true }); const sourceConfig = buildSourceConfig(); sourceConfig.database = { mode: "postgres", embeddedPostgresDataDir: path.join(sourceConfigDir, "db"), embeddedPostgresPort: 54329, backup: { enabled: true, intervalMinutes: 60, retentionDays: 30, dir: path.join(sourceConfigDir, "backups"), }, connectionString: sourceDb.connectionString, }; sourceConfig.logging.logDir = path.join(sourceConfigDir, "logs"); sourceConfig.storage.localDisk.baseDir = path.join(sourceConfigDir, "storage"); sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath; fs.writeFileSync(sourceConfigPath, JSON.stringify(sourceConfig, null, 2) + "\n", "utf8"); fs.writeFileSync(sourceEnvPath, "", "utf8"); fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8"); process.chdir(worktreeRoot); await worktreeInitCommand({ name: "PAP-999-auth-seed", home: worktreeHome, fromConfig: sourceConfigPath, force: true, }); const targetConfig = JSON.parse( fs.readFileSync(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8"), ) as PaperclipConfig; const manifestText = fs.readFileSync( path.join(worktreeRoot, ".paperclip", "seed-manifest.json"), "utf8", ); expect(JSON.parse(manifestText)).toMatchObject({ version: 2, seedMode: "minimal", state: "verified", phase: "complete", }); expect(manifestText).toContain(`Validated migration ${sourceMigrationRevision}`); expect(manifestText).not.toContain("fixture-password-hash"); expect(manifestText).not.toContain("source-master-key"); const { default: EmbeddedPostgres } = await import("embedded-postgres"); const targetPg = new EmbeddedPostgres({ databaseDir: targetConfig.database.embeddedPostgresDataDir, user: "paperclip", password: "paperclip", port: targetConfig.database.embeddedPostgresPort, persistent: true, initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"], onLog: () => {}, onError: () => {}, }); await targetPg.start(); onTestFinished(() => targetPg.stop()); const targetDb = createDb( `postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`, ); const seededUsers = await targetDb.select().from(authUsers); expect(seededUsers.some((row) => row.email === "existing@paperclip.ing")).toBe(true); const restoredEvents = await targetDb.$client` SELECT id, action FROM resource_lifecycle_events WHERE resource_id = ${legacyAgentId} ORDER BY id `; expect(restoredEvents.map(row => row.action)).toEqual(["pause", "create"]); expect(Number(restoredEvents[1].id)).toBeGreaterThan(Number(restoredEvents[0].id)); const [identity] = await targetDb.$client` SELECT is_identity FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'resource_lifecycle_events' AND column_name = 'id' `; expect(identity.is_identity).toBe("YES"); }, ); it("avoids ports already claimed by sibling worktree instance configs", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-claimed-ports-")); const repoRoot = path.join(tempRoot, "repo"); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); const siblingInstanceRoot = path.join(homeDir, "instances", "existing-worktree"); const originalCwd = process.cwd(); try { fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(siblingInstanceRoot, { recursive: true }); fs.writeFileSync( path.join(siblingInstanceRoot, "config.json"), JSON.stringify( { ...buildSourceConfig(), database: { mode: "embedded-postgres", embeddedPostgresDataDir: path.join(siblingInstanceRoot, "db"), embeddedPostgresPort: 54330, backup: { enabled: true, intervalMinutes: 60, retentionDays: 30, dir: path.join(siblingInstanceRoot, "backups"), }, }, logging: { mode: "file", logDir: path.join(siblingInstanceRoot, "logs"), }, server: { deploymentMode: "authenticated", exposure: "private", host: "127.0.0.1", port: 3101, allowedHostnames: ["localhost"], serveUi: true, }, storage: { provider: "local_disk", localDisk: { baseDir: path.join(siblingInstanceRoot, "storage"), }, s3: { bucket: "paperclip", region: "us-east-1", prefix: "", forcePathStyle: false, }, }, secrets: { provider: "local_encrypted", strictMode: false, localEncrypted: { keyFilePath: path.join(siblingInstanceRoot, "secrets", "master.key"), }, }, }, null, 2, ) + "\n", ); process.chdir(repoRoot); await worktreeInitCommand({ seed: false, fromConfig: path.join(tempRoot, "missing", "config.json"), home: homeDir, }); const config = JSON.parse(fs.readFileSync(path.join(repoRoot, ".paperclip", "config.json"), "utf8")); expect(config.server.port).toBeGreaterThan(3101); expect(config.database.embeddedPostgresPort).not.toBe(54330); expect(config.database.embeddedPostgresPort).not.toBe(config.server.port); expect(config.database.embeddedPostgresPort).toBeGreaterThan(54330); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("reserves distinct ports for postgres-mode siblings under a custom worktree parent", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-custom-parent-")); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); const customParentDir = path.join(tempRoot, "custom", "workspace-lanes"); const firstWorktreeRoot = path.join(customParentDir, "lane-one"); const secondWorktreeRoot = path.join(customParentDir, "lane-two"); const missingSourceConfig = path.join(tempRoot, "missing", "config.json"); const firstConfigPath = path.join(firstWorktreeRoot, ".paperclip", "config.json"); const secondConfigPath = path.join(secondWorktreeRoot, ".paperclip", "config.json"); const originalCwd = process.cwd(); try { fs.mkdirSync(firstWorktreeRoot, { recursive: true }); fs.mkdirSync(secondWorktreeRoot, { recursive: true }); process.chdir(firstWorktreeRoot); await worktreeInitCommand({ name: "lane-one", seed: false, fromConfig: missingSourceConfig, home: homeDir, }); const firstConfig = JSON.parse(fs.readFileSync(firstConfigPath, "utf8")); firstConfig.database = { ...firstConfig.database, mode: "postgres", connectionString: "postgres://paperclip:paperclip@127.0.0.1:54330/paperclip", }; fs.writeFileSync(firstConfigPath, `${JSON.stringify(firstConfig, null, 2)}\n`, "utf8"); process.chdir(secondWorktreeRoot); await worktreeInitCommand({ name: "lane-two", seed: false, fromConfig: missingSourceConfig, home: homeDir, }); const secondConfig = JSON.parse(fs.readFileSync(secondConfigPath, "utf8")); const registry = JSON.parse( fs.readFileSync(path.join(homeDir, "worktree-port-reservations.json"), "utf8"), ); expect(secondConfig.server.port).not.toBe(firstConfig.server.port); expect(secondConfig.database.embeddedPostgresPort).not.toBe( firstConfig.database.embeddedPostgresPort, ); expect(registry.configPaths).toEqual([firstConfigPath, secondConfigPath].sort()); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("defaults the seed source config to the current repo-local Paperclip config", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-config-")); const repoRoot = path.join(tempRoot, "repo"); const localConfigPath = path.join(repoRoot, ".paperclip", "config.json"); const originalCwd = process.cwd(); const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG; try { fs.mkdirSync(path.dirname(localConfigPath), { recursive: true }); fs.writeFileSync(localConfigPath, JSON.stringify(buildSourceConfig()), "utf8"); delete process.env.PAPERCLIP_CONFIG; process.chdir(repoRoot); expect(fs.realpathSync(resolveSourceConfigPath({}))).toBe(fs.realpathSync(localConfigPath)); } finally { process.chdir(originalCwd); if (originalPaperclipConfig === undefined) { delete process.env.PAPERCLIP_CONFIG; } else { process.env.PAPERCLIP_CONFIG = originalPaperclipConfig; } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("preserves the source config path across worktree:make cwd changes", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-override-")); const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "target"); const originalCwd = process.cwd(); const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG; try { fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(targetRoot, { recursive: true }); fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig()), "utf8"); delete process.env.PAPERCLIP_CONFIG; process.chdir(targetRoot); expect(resolveSourceConfigPath({ sourceConfigPathOverride: sourceConfigPath })).toBe( path.resolve(sourceConfigPath), ); } finally { process.chdir(originalCwd); if (originalPaperclipConfig === undefined) { delete process.env.PAPERCLIP_CONFIG; } else { process.env.PAPERCLIP_CONFIG = originalPaperclipConfig; } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("requires an explicit reseed source", () => { expect(() => resolveWorktreeReseedSource({})).toThrow( "Pass --from or --from-config/--from-instance explicitly so the reseed source is unambiguous.", ); }); it("rejects mixed reseed source selectors", () => { expect(() => resolveWorktreeReseedSource({ from: "current", fromInstance: "default", })).toThrow( "Use either --from or --from-config/--from-data-dir/--from-instance, not both.", ); }); it("derives worktree reseed target paths from the adjacent env file", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-")); const worktreeRoot = path.join(tempRoot, "repo"); const configPath = path.join(worktreeRoot, ".paperclip", "config.json"); const envPath = path.join(worktreeRoot, ".paperclip", ".env"); try { fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8"); fs.writeFileSync( envPath, [ "PAPERCLIP_HOME=/tmp/paperclip-worktrees", "PAPERCLIP_INSTANCE_ID=pap-1132-chat", ].join("\n"), "utf8", ); expect( resolveWorktreeReseedTargetPaths({ configPath, rootPath: worktreeRoot, }), ).toMatchObject({ cwd: worktreeRoot, homeDir: "/tmp/paperclip-worktrees", instanceId: "pap-1132-chat", }); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("rejects reseed targets without worktree env metadata", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-missing-")); const worktreeRoot = path.join(tempRoot, "repo"); const configPath = path.join(worktreeRoot, ".paperclip", "config.json"); try { fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8"); fs.writeFileSync(path.join(worktreeRoot, ".paperclip", ".env"), "", "utf8"); expect(() => resolveWorktreeReseedTargetPaths({ configPath, rootPath: worktreeRoot, })).toThrow("does not look like a worktree-local Paperclip instance"); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("uses transformed backups for both seed modes to omit agent identities", () => { expect(resolveWorktreeSeedBackupEngine(resolveWorktreeSeedPlan("full"))).toBe("javascript"); expect(resolveWorktreeSeedBackupEngine(resolveWorktreeSeedPlan("minimal"))).toBe("javascript"); }); itEmbeddedPostgres("reseed preserves the current worktree ports, instance id, and branding", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-")); const repoRoot = path.join(tempRoot, "repo"); const sourceRoot = path.join(tempRoot, "source"); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); const currentInstanceId = "existing-worktree"; const currentPaths = resolveWorktreeLocalPaths({ cwd: repoRoot, homeDir, instanceId: currentInstanceId, }); const sourcePaths = resolveWorktreeLocalPaths({ cwd: sourceRoot, homeDir: path.join(tempRoot, ".paperclip-source"), instanceId: "default", }); const originalCwd = process.cwd(); const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG; const currentDatabaseReservation = await reserveTestPort(); const currentDatabasePort = currentDatabaseReservation.port; const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-reseed-source-"); onTestFinished(() => sourceDb.cleanup()); try { fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true }); fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(sourceRoot, { recursive: true }); const currentConfig = buildWorktreeConfig({ sourceConfig: buildSourceConfig(), paths: currentPaths, serverPort: 3114, databasePort: currentDatabasePort, }); const sourceConfig = buildSourceConfig(); sourceConfig.database = { mode: "postgres", embeddedPostgresDataDir: sourcePaths.embeddedPostgresDataDir, embeddedPostgresPort: 54329, backup: { enabled: true, intervalMinutes: 60, retentionDays: 30, dir: sourcePaths.backupDir, }, connectionString: sourceDb.connectionString, }; sourceConfig.logging.logDir = sourcePaths.logDir; sourceConfig.storage.localDisk.baseDir = sourcePaths.storageDir; sourceConfig.secrets.localEncrypted.keyFilePath = sourcePaths.secretsKeyFilePath; await seedValidWorktreeSource(sourceDb.connectionString); fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8"); fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8"); fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8"); const worktreeSentinelPath = path.join(repoRoot, "user-worktree-file.txt"); fs.writeFileSync(worktreeSentinelPath, "preserve me", "utf8"); fs.writeFileSync( currentPaths.envPath, [ `PAPERCLIP_HOME=${homeDir}`, `PAPERCLIP_INSTANCE_ID=${currentInstanceId}`, "PAPERCLIP_WORKTREE_NAME=existing-name", "PAPERCLIP_WORKTREE_COLOR=\"#112233\"", ].join("\n"), "utf8", ); delete process.env.PAPERCLIP_CONFIG; process.chdir(repoRoot); await currentDatabaseReservation.release(); await worktreeReseedCommand({ fromConfig: sourcePaths.configPath, yes: true, backupTarget: true, }); const rewrittenConfig = JSON.parse(fs.readFileSync(currentPaths.configPath, "utf8")); const rewrittenEnv = fs.readFileSync(currentPaths.envPath, "utf8"); expect(rewrittenConfig.server.port).toBe(3114); expect(rewrittenConfig.database.embeddedPostgresPort).toBe(currentDatabasePort); expect(rewrittenConfig.database.embeddedPostgresDataDir).toBe(currentPaths.embeddedPostgresDataDir); expect(rewrittenEnv).toContain(`PAPERCLIP_INSTANCE_ID=${currentInstanceId}`); expect(rewrittenEnv).toContain("PAPERCLIP_WORKTREE_NAME=existing-name"); expect(rewrittenEnv).toContain("PAPERCLIP_WORKTREE_COLOR=\"#112233\""); expect(fs.readFileSync(worktreeSentinelPath, "utf8")).toBe("preserve me"); expect( fs.readdirSync(path.join(currentPaths.backupDir, "repair")).some((name) => name.endsWith(".sql.gz")), ).toBe(true); } finally { await currentDatabaseReservation.release(); process.chdir(originalCwd); if (originalPaperclipConfig === undefined) { delete process.env.PAPERCLIP_CONFIG; } else { process.env.PAPERCLIP_CONFIG = originalPaperclipConfig; } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("restores the current worktree config and instance data if reseed fails", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-")); const repoRoot = path.join(tempRoot, "repo"); const sourceRoot = path.join(tempRoot, "source"); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); const currentInstanceId = "rollback-worktree"; const currentPaths = resolveWorktreeLocalPaths({ cwd: repoRoot, homeDir, instanceId: currentInstanceId, }); const sourcePaths = resolveWorktreeLocalPaths({ cwd: sourceRoot, homeDir: path.join(tempRoot, ".paperclip-source"), instanceId: "default", }); const originalCwd = process.cwd(); const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG; try { fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true }); fs.mkdirSync(currentPaths.instanceRoot, { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true }); fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(sourceRoot, { recursive: true }); const currentConfig = buildWorktreeConfig({ sourceConfig: buildSourceConfig(), paths: currentPaths, serverPort: 3114, databasePort: 54341, }); const sourceConfig = { ...buildSourceConfig(), database: { mode: "postgres", connectionString: "", }, secrets: { provider: "local_encrypted", strictMode: false, localEncrypted: { keyFilePath: sourcePaths.secretsKeyFilePath, }, }, } as PaperclipConfig; fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8"); fs.writeFileSync(currentPaths.envPath, `PAPERCLIP_HOME=${homeDir}\nPAPERCLIP_INSTANCE_ID=${currentInstanceId}\n`, "utf8"); fs.writeFileSync(path.join(currentPaths.instanceRoot, "marker.txt"), "keep me", "utf8"); fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8"); fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8"); delete process.env.PAPERCLIP_CONFIG; process.chdir(repoRoot); await expect(worktreeReseedCommand({ fromConfig: sourcePaths.configPath, yes: true, })).rejects.toThrow("Source instance uses postgres mode but has no connection string"); const restoredConfig = JSON.parse(fs.readFileSync(currentPaths.configPath, "utf8")); const restoredEnv = fs.readFileSync(currentPaths.envPath, "utf8"); const restoredMarker = fs.readFileSync(path.join(currentPaths.instanceRoot, "marker.txt"), "utf8"); expect(restoredConfig.server.port).toBe(3114); expect(restoredConfig.database.embeddedPostgresPort).toBe(54341); expect(restoredEnv).toContain(`PAPERCLIP_INSTANCE_ID=${currentInstanceId}`); expect(restoredMarker).toBe("keep me"); } finally { process.chdir(originalCwd); if (originalPaperclipConfig === undefined) { delete process.env.PAPERCLIP_CONFIG; } else { process.env.PAPERCLIP_CONFIG = originalPaperclipConfig; } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("rebinds same-repo workspace paths onto the current worktree root", () => { expect( rebindWorkspaceCwd({ sourceRepoRoot: "/Users/example/paperclip", targetRepoRoot: "/Users/example/paperclip-pr-432", workspaceCwd: "/Users/example/paperclip", }), ).toBe("/Users/example/paperclip-pr-432"); expect( rebindWorkspaceCwd({ sourceRepoRoot: "/Users/example/paperclip", targetRepoRoot: "/Users/example/paperclip-pr-432", workspaceCwd: "/Users/example/paperclip/packages/db", }), ).toBe("/Users/example/paperclip-pr-432/packages/db"); }); it("does not rebind paths outside the source repo root", () => { expect( rebindWorkspaceCwd({ sourceRepoRoot: "/Users/example/paperclip", targetRepoRoot: "/Users/example/paperclip-pr-432", workspaceCwd: "/Users/example/other-project", }), ).toBeNull(); }); it("copies shared git hooks into a linked worktree git dir", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-hooks-")); const repoRoot = path.join(tempRoot, "repo"); const worktreePath = path.join(tempRoot, "repo-feature"); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); const sourceHooksDir = path.join(repoRoot, ".git", "hooks"); const sourceHookPath = path.join(sourceHooksDir, "pre-commit"); const sourceTokensPath = path.join(sourceHooksDir, "forbidden-tokens.txt"); fs.writeFileSync(sourceHookPath, "#!/usr/bin/env bash\nexit 0\n", { encoding: "utf8", mode: 0o755 }); fs.chmodSync(sourceHookPath, 0o755); fs.writeFileSync(sourceTokensPath, "secret-token\n", "utf8"); execFileSync("git", ["worktree", "add", "--detach", worktreePath], { cwd: repoRoot, stdio: "ignore" }); const copied = copyGitHooksToWorktreeGitDir(worktreePath); const worktreeGitDir = execFileSync("git", ["rev-parse", "--git-dir"], { cwd: worktreePath, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); const resolvedSourceHooksDir = fs.realpathSync(sourceHooksDir); const resolvedTargetHooksDir = fs.realpathSync(path.resolve(worktreePath, worktreeGitDir, "hooks")); const targetHookPath = path.join(resolvedTargetHooksDir, "pre-commit"); const targetTokensPath = path.join(resolvedTargetHooksDir, "forbidden-tokens.txt"); expect(copied).toMatchObject({ sourceHooksPath: resolvedSourceHooksDir, targetHooksPath: resolvedTargetHooksDir, copied: true, }); expect(fs.readFileSync(targetHookPath, "utf8")).toBe("#!/usr/bin/env bash\nexit 0\n"); expect(fs.statSync(targetHookPath).mode & 0o111).not.toBe(0); expect(fs.readFileSync(targetTokensPath, "utf8")).toBe("secret-token\n"); } finally { execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, stdio: "ignore" }); fs.rmSync(tempRoot, { recursive: true, force: true }); } }, 15_000); it("creates and initializes a worktree from the top-level worktree:make command", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-make-")); const repoRoot = path.join(tempRoot, "repo"); const fakeHome = path.join(tempRoot, "home"); const worktreePath = path.join(fakeHome, "paperclip-make-test"); const originalCwd = process.cwd(); const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(fakeHome); try { fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(fakeHome, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); process.chdir(repoRoot); await worktreeMakeCommand("paperclip-make-test", { seed: false, home: path.join(tempRoot, ".paperclip-worktrees"), }); expect(fs.existsSync(path.join(worktreePath, ".git"))).toBe(true); expect(fs.existsSync(path.join(worktreePath, ".paperclip", "config.json"))).toBe(true); expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true); } finally { process.chdir(originalCwd); homedirSpy.mockRestore(); fs.rmSync(tempRoot, { recursive: true, force: true }); } }, 20_000); it("no-ops on the primary checkout unless --branch is provided", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-primary-")); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); process.chdir(repoRoot); await worktreeRepairCommand({}); expect(fs.existsSync(path.join(repoRoot, ".paperclip", "config.json"))).toBe(false); expect(fs.existsSync(path.join(repoRoot, ".paperclip", "worktrees"))).toBe(false); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("repairs the current linked worktree when Paperclip metadata is missing", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-current-")); const repoRoot = path.join(tempRoot, "repo"); const worktreePath = path.join(repoRoot, ".paperclip", "worktrees", "repair-me"); const sourceConfigPath = path.join(tempRoot, "source-config.json"); const worktreeHome = path.join(tempRoot, ".paperclip-worktrees"); const worktreePaths = resolveWorktreeLocalPaths({ cwd: worktreePath, homeDir: worktreeHome, instanceId: sanitizeWorktreeInstanceId(path.basename(worktreePath)), }); const originalCwd = process.cwd(); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); execFileSync("git", ["worktree", "add", "-b", "repair-me", worktreePath, "HEAD"], { cwd: repoRoot, stdio: "ignore", }); fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8"); fs.mkdirSync(worktreePaths.instanceRoot, { recursive: true }); fs.writeFileSync(path.join(worktreePaths.instanceRoot, "marker.txt"), "stale", "utf8"); process.chdir(worktreePath); await worktreeRepairCommand({ fromConfig: sourceConfigPath, home: worktreeHome, noSeed: true, }); expect(fs.existsSync(path.join(worktreePath, ".paperclip", "config.json"))).toBe(true); expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true); expect(fs.existsSync(path.join(worktreePaths.instanceRoot, "marker.txt"))).toBe(false); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); } }, 20_000); it("creates and repairs a missing branch worktree when --branch is provided", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-branch-")); const repoRoot = path.join(tempRoot, "repo"); const sourceConfigPath = path.join(tempRoot, "source-config.json"); const worktreeHome = path.join(tempRoot, ".paperclip-worktrees"); const originalCwd = process.cwd(); const expectedWorktreePath = path.join(repoRoot, ".paperclip", "worktrees", "feature-repair-me"); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8"); process.chdir(repoRoot); await worktreeRepairCommand({ branch: "feature/repair-me", fromConfig: sourceConfigPath, home: worktreeHome, noSeed: true, }); expect(fs.existsSync(path.join(expectedWorktreePath, ".git"))).toBe(true); expect(fs.existsSync(path.join(expectedWorktreePath, ".paperclip", "config.json"))).toBe(true); expect(fs.existsSync(path.join(expectedWorktreePath, ".paperclip", ".env"))).toBe(true); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); } }, 20_000); }); describeEmbeddedPostgres("pauseSeededScheduledRoutines", () => { it("pauses only routines with enabled schedule triggers", async () => { const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-routines-"); const db = createDb(tempDb.connectionString); const companyId = randomUUID(); const projectId = randomUUID(); const agentId = randomUUID(); const activeScheduledRoutineId = randomUUID(); const activeApiRoutineId = randomUUID(); const pausedScheduledRoutineId = randomUUID(); const archivedScheduledRoutineId = randomUUID(); const disabledScheduleRoutineId = randomUUID(); try { 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: "Coder", adapterType: "process", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(projects).values({ id: projectId, companyId, name: "Project", status: "in_progress", }); await db.insert(routines).values([ { id: activeScheduledRoutineId, companyId, projectId, assigneeAgentId: agentId, title: "Active scheduled", status: "active", }, { id: activeApiRoutineId, companyId, projectId, assigneeAgentId: agentId, title: "Active API", status: "active", }, { id: pausedScheduledRoutineId, companyId, projectId, assigneeAgentId: agentId, title: "Paused scheduled", status: "paused", }, { id: archivedScheduledRoutineId, companyId, projectId, assigneeAgentId: agentId, title: "Archived scheduled", status: "archived", }, { id: disabledScheduleRoutineId, companyId, projectId, assigneeAgentId: agentId, title: "Disabled schedule", status: "active", }, ]); await db.insert(routineTriggers).values([ { companyId, routineId: activeScheduledRoutineId, kind: "schedule", enabled: true, cronExpression: "0 9 * * *", timezone: "UTC", }, { companyId, routineId: activeApiRoutineId, kind: "api", enabled: true, }, { companyId, routineId: pausedScheduledRoutineId, kind: "schedule", enabled: true, cronExpression: "0 10 * * *", timezone: "UTC", }, { companyId, routineId: archivedScheduledRoutineId, kind: "schedule", enabled: true, cronExpression: "0 11 * * *", timezone: "UTC", }, { companyId, routineId: disabledScheduleRoutineId, kind: "schedule", enabled: false, cronExpression: "0 12 * * *", timezone: "UTC", }, ]); const pausedCount = await pauseSeededScheduledRoutines(tempDb.connectionString); expect(pausedCount).toBe(1); const rows = await db.select({ id: routines.id, status: routines.status }).from(routines); const statusById = new Map(rows.map((row) => [row.id, row.status])); expect(statusById.get(activeScheduledRoutineId)).toBe("paused"); expect(statusById.get(activeApiRoutineId)).toBe("active"); expect(statusById.get(pausedScheduledRoutineId)).toBe("paused"); expect(statusById.get(archivedScheduledRoutineId)).toBe("archived"); expect(statusById.get(disabledScheduleRoutineId)).toBe("active"); } finally { await db.$client?.end?.({ timeout: 5 }).catch(() => undefined); await tempDb.cleanup(); } }, 20_000); });