chore: improve worktree tooling and security docs

This commit is contained in:
Dotta
2026-04-10 22:26:30 -05:00
parent 548721248e
commit 8bdf4081ee
17 changed files with 1100 additions and 123 deletions

View File

@@ -2,10 +2,20 @@ 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 { afterEach, describe, expect, it, vi } from "vitest";
import {
agents,
companies,
createDb,
projects,
routines,
routineTriggers,
} from "@paperclipai/db";
import {
copyGitHooksToWorktreeGitDir,
copySeededSecretsKey,
pauseSeededScheduledRoutines,
readSourceAttachmentBody,
rebindWorkspaceCwd,
resolveSourceConfigPath,
@@ -28,9 +38,21 @@ import {
sanitizeWorktreeInstanceId,
} from "../commands/worktree-lib.js";
import type { PaperclipConfig } from "../config/schema.js";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const ORIGINAL_CWD = process.cwd();
const ORIGINAL_ENV = { ...process.env };
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres worktree CLI tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
afterEach(() => {
process.chdir(ORIGINAL_CWD);
@@ -823,3 +845,138 @@ describe("worktree helpers", () => {
}
}, 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);
});

View File

@@ -39,6 +39,8 @@ import {
issues,
projectWorkspaces,
projects,
routines,
routineTriggers,
runDatabaseBackup,
runDatabaseRestore,
createEmbeddedPostgresLogBuffer,
@@ -922,6 +924,36 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P
};
}
export async function pauseSeededScheduledRoutines(connectionString: string): Promise<number> {
const db = createDb(connectionString);
try {
const scheduledRoutineIds = await db
.selectDistinct({ routineId: routineTriggers.routineId })
.from(routineTriggers)
.where(and(eq(routineTriggers.kind, "schedule"), eq(routineTriggers.enabled, true)));
const idsToPause = scheduledRoutineIds
.map((row) => row.routineId)
.filter((value): value is string => Boolean(value));
if (idsToPause.length === 0) {
return 0;
}
const paused = await db
.update(routines)
.set({
status: "paused",
updatedAt: new Date(),
})
.where(and(inArray(routines.id, idsToPause), sql`${routines.status} <> 'paused'`, sql`${routines.status} <> 'archived'`))
.returning({ id: routines.id });
return paused.length;
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
}
}
async function seedWorktreeDatabase(input: {
sourceConfigPath: string;
sourceConfig: PaperclipConfig;
@@ -979,6 +1011,7 @@ async function seedWorktreeDatabase(input: {
backupFile: backup.backupFile,
});
await applyPendingMigrations(targetConnectionString);
await pauseSeededScheduledRoutines(targetConnectionString);
const reboundWorkspaces = await rebindSeededProjectWorkspaces({
targetConnectionString,
currentCwd: input.targetPaths.cwd,