Files
PaperClipAI/cli/src/__tests__/worktree.test.ts
T

2648 lines
106 KiB
TypeScript

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>): 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",
});
await db.insert(companies).values({
id: companyId,
name: "Seed Source",
issuePrefix: "SEED",
requireBoardApprovalForNewAgents: 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<void> }> {
const server = createServer();
server.unref();
await new Promise<void>((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<void>((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).toEqual({
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 <worktree> 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 <worktree> 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);
});