mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
feat(dev): support explicitly empty worktree test instances
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
88dddb8298
commit
1d89022634
3 files changed
+44
-11
No files matched your search
@@ -1472,6 +1472,32 @@ describe("worktree helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
} 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");
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { createServer } from "node:net";
|
||||
import { Readable } from "node:stream";
|
||||
import * as p from "@clack/prompts";
|
||||
@@ -118,6 +118,7 @@ type WorktreeInitOptions = {
|
||||
serverPort?: number;
|
||||
dbPort?: number;
|
||||
seed?: boolean;
|
||||
empty?: boolean;
|
||||
seedMode?: string;
|
||||
preserveLiveWork?: boolean;
|
||||
force?: boolean;
|
||||
@@ -2434,6 +2435,7 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
|
||||
rmSync(paths.configPath, { force: true });
|
||||
rmSync(paths.envPath, { force: true });
|
||||
const seedMarkers = resolveWorktreeSeedMarkerPaths(paths.configPath);
|
||||
rmSync(seedMarkers.manifest, { force: true });
|
||||
rmSync(seedMarkers.pending, { force: true });
|
||||
rmSync(seedMarkers.complete, { force: true });
|
||||
rmSync(paths.instanceRoot, { recursive: true, force: true });
|
||||
@@ -2481,18 +2483,20 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
|
||||
};
|
||||
},
|
||||
);
|
||||
markWorktreeSeedPending({
|
||||
configPath: paths.configPath,
|
||||
sourceConfigPath,
|
||||
targetInstanceId: instanceId,
|
||||
seedMode,
|
||||
});
|
||||
if (!opts.empty) {
|
||||
markWorktreeSeedPending({
|
||||
configPath: paths.configPath,
|
||||
sourceConfigPath,
|
||||
targetInstanceId: instanceId,
|
||||
seedMode,
|
||||
});
|
||||
}
|
||||
const sourceEnvEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(sourceConfigPath));
|
||||
const existingAgentJwtSecret =
|
||||
nonEmpty(sourceEnvEntries.PAPERCLIP_AGENT_JWT_SECRET) ??
|
||||
opts.empty ? randomBytes(32).toString("base64url") : nonEmpty(sourceEnvEntries.PAPERCLIP_AGENT_JWT_SECRET) ??
|
||||
nonEmpty(process.env.PAPERCLIP_AGENT_JWT_SECRET);
|
||||
const existingToolActionSigningSecret =
|
||||
nonEmpty(sourceEnvEntries.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET) ??
|
||||
opts.empty ? randomBytes(32).toString("base64url") : nonEmpty(sourceEnvEntries.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET) ??
|
||||
nonEmpty(process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET);
|
||||
mergePaperclipEnvEntries(
|
||||
{
|
||||
@@ -2511,7 +2515,7 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
|
||||
let seedExecutionQuarantineSummary: SeededWorktreeExecutionQuarantineSummary | null = null;
|
||||
let pausedScheduledRoutineCount: number | null = null;
|
||||
let reboundWorkspaceSummary: SeedWorktreeDatabaseResult["reboundWorkspaces"] = [];
|
||||
if (opts.seed !== false) {
|
||||
if (!opts.empty && opts.seed !== false) {
|
||||
if (!sourceConfig) {
|
||||
throw new Error(
|
||||
`Cannot seed worktree database because source config was not found at ${sourceConfigPath}. Use --no-seed or provide --from-config.`,
|
||||
@@ -4456,6 +4460,7 @@ export function registerWorktreeCommands(program: Command): void {
|
||||
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
|
||||
.option("--preserve-live-work", "Do not quarantine copied agent work or workspace runtime services in the seeded worktree", false)
|
||||
.option("--no-seed", "Skip database seeding from the source instance")
|
||||
.option("--empty", "Create an empty instance without immediate or deferred database copying", false)
|
||||
.option("--force", "Replace existing repo-local config and isolated instance data", false)
|
||||
.action(worktreeMakeCommand);
|
||||
|
||||
@@ -4473,6 +4478,7 @@ export function registerWorktreeCommands(program: Command): void {
|
||||
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
|
||||
.option("--preserve-live-work", "Do not quarantine copied agent work or workspace runtime services in the seeded worktree", false)
|
||||
.option("--no-seed", "Skip database seeding from the source instance")
|
||||
.option("--empty", "Create an empty instance without immediate or deferred database copying", false)
|
||||
.option("--force", "Replace existing repo-local config and isolated instance data", false)
|
||||
.action(worktreeInitCommand);
|
||||
|
||||
|
||||
+2
-1
@@ -885,7 +885,8 @@ Seed modes:
|
||||
|
||||
- `minimal` keeps core app state like companies, projects, issues, comments, approvals, and auth state, preserves schema for all tables, but omits row data from heavy operational history such as heartbeat runs, wake requests, activity logs, runtime services, and agent session state
|
||||
- `full` makes a full logical clone of the source instance
|
||||
- `--no-seed` creates an empty isolated instance
|
||||
- `--no-seed` defers database copying until the worktree is first used
|
||||
- `--empty` creates an empty isolated instance, with fresh signing secrets and no deferred copy; use this for synthetic test drives
|
||||
|
||||
Seeded worktree instances quarantine copied live execution by default for both `minimal` and `full` seeds. During restore, Paperclip disables copied agent timer heartbeats, resets copied `running` agents to `idle`, blocks and unassigns copied agent-owned `in_progress` issues, and unassigns copied agent-owned `todo`/`in_review` issues. This keeps a freshly booted worktree from starting agents for work already owned by the source instance. Pass `--preserve-live-work` only when you intentionally want the isolated worktree to resume copied assignments.
|
||||
|
||||
|
||||
Reference in new issue
Block a user