Files
PaperClipAI/server/src/__tests__/execution-workspace-policy.test.ts
417336f8be fix(workspaces): attach PR preparation to existing branches (#11703)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Execution workspaces isolate an agent task from the primary
checkout.
> - Pull request preparation can need a branch that already contains
completed work.
> - The workspace policy could not require an exact existing branch.
> - Workspace cleanup also treated worktree creation as branch
ownership.
> - This pull request adds an exact existing-branch policy and separate
branch ownership metadata.
> - The benefit is safe pull request preparation that preserves every
existing commit and operator-owned branch.

## Linked Issues or Issue Description

**What happened?**

A pull request preparation run could not pin its execution workspace to
an exact existing branch. Workspace reuse and cleanup could also confuse
worktree creation with branch ownership.

**Expected behavior**

The run must attach only to the requested branch in an isolated Git
worktree. It must fail if the branch is missing, busy, or inconsistent.
Cleanup must not delete a branch that Paperclip does not own.

**Steps to reproduce**

1. Create a branch that contains completed work.
2. Configure a pull request preparation task to use that branch.
3. Start the task and observe that the prior policy cannot require the
exact branch.

**Paperclip version or commit**

This behavior reproduces on the base revision before this pull request.

**Deployment mode**

Local development with isolated Git worktrees.

## What Changed

- Add `existingBranch` to the execution workspace policy and shared
validation contracts.
- Require `existingBranch` to use an isolated Git worktree and reject
conflicting branch templates.
- Attach to the exact branch without creating, renaming, resetting, or
deleting it.
- Track branch ownership separately from worktree creation and use that
ownership during cleanup.
- Return HTTP 422 for invalid existing-branch settings on every
issue-producing route.
- Add a bounded repair script for existing pull request preparation
tasks.
- Add focused policy, route, heartbeat, runtime, and ready-comment
tests.
- Document the exact-branch behavior and safety rules.

## Verification

- `pnpm exec vitest run
server/src/__tests__/execution-workspace-policy.test.ts
server/src/__tests__/heartbeat-workspace-session.test.ts
server/src/__tests__/issue-existing-branch-validation-status.test.ts
server/src/__tests__/workspace-runtime.test.ts
server/src/services/workspace-runtime-exposure.test.ts
server/src/services/workspace-runtime-ready-comment.test.ts` passed 335
tests.
- `pnpm -r typecheck` passed for all workspace projects.
- `pnpm test:run` passed 4,431 tests. Two unrelated embedded-Postgres
setup hooks timed out under aggregate load. Their isolated rerun passed
74 tests.
- `pnpm build` passed for all workspace projects.
- The two review regressions passed with 139 unrelated tests skipped.
- All latest-head CI gates passed after one unrelated timing-sensitive
test passed on rerun.
- Greptile scored the latest head 5/5 with no unresolved review threads.

## Risks

- Invalid workspace settings now return HTTP 422 instead of a generic
validation response.
- The exact branch must already exist and must not be checked out by
another worktree.
- The new policy fails closed when it cannot prove branch identity or
ownership.
- This change has no database migration.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex from the GPT-5 family assisted with this change. The
runtime did not expose its exact deployment ID or context window. The
agent used high-reasoning mode, repository tools, shell execution, and
code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 17:23:18 -05:00

535 lines
19 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
issueExecutionWorkspaceSettingsSchema,
projectExecutionWorkspacePolicySchema,
} from "@paperclipai/shared";
import {
buildExecutionWorkspaceAdapterConfig,
defaultIssueExecutionWorkspaceSettingsForProject,
gateProjectExecutionWorkspacePolicy,
isUnrunnableWorktreeCombo,
issueExecutionWorkspaceModeForPersistedWorkspace,
parseIssueExecutionWorkspaceSettings,
parseProjectExecutionWorkspacePolicy,
ManagedSandboxUnavailableError,
resolveExecutionWorkspaceEnvironmentId,
resolvePinnedIssueWorkspaceStrategyType,
resolveExecutionWorkspaceMode,
resolveSharedWorkspaceConcurrency,
selectEnvironmentExecutionWorkspaceSettings,
} from "../services/execution-workspace-policy.ts";
describe("execution workspace policy helpers", () => {
it("defaults new issue settings from enabled project policy", () => {
expect(
defaultIssueExecutionWorkspaceSettingsForProject({
enabled: true,
defaultMode: "isolated_workspace",
}),
).toEqual({ mode: "isolated_workspace" });
expect(
defaultIssueExecutionWorkspaceSettingsForProject({
enabled: true,
defaultMode: "shared_workspace",
}),
).toEqual({ mode: "shared_workspace" });
expect(defaultIssueExecutionWorkspaceSettingsForProject(null)).toBeNull();
});
it("prefers explicit issue mode over project policy and legacy overrides", () => {
expect(
resolveExecutionWorkspaceMode({
projectPolicy: { enabled: true, defaultMode: "shared_workspace" },
issueSettings: { mode: "isolated_workspace" },
legacyUseProjectWorkspace: false,
}),
).toBe("isolated_workspace");
});
it("resolves shared-workspace concurrency from issue override, project policy, then auto", () => {
expect(
resolveSharedWorkspaceConcurrency({
projectPolicy: { enabled: true, sharedWorkspaceConcurrency: "serialize" },
issueSettings: { sharedWorkspaceConcurrency: "allow" },
}),
).toBe("allow");
expect(
resolveSharedWorkspaceConcurrency({
projectPolicy: { enabled: true, sharedWorkspaceConcurrency: "serialize" },
issueSettings: null,
}),
).toBe("serialize");
expect(
resolveSharedWorkspaceConcurrency({
projectPolicy: { enabled: false, sharedWorkspaceConcurrency: "serialize" },
issueSettings: null,
}),
).toBe("auto");
expect(resolveSharedWorkspaceConcurrency({ projectPolicy: null, issueSettings: null })).toBe("auto");
});
it("validates the shared-workspace concurrency enum on project and issue settings", () => {
expect(projectExecutionWorkspacePolicySchema.parse({
enabled: true,
sharedWorkspaceConcurrency: "auto",
}).sharedWorkspaceConcurrency).toBe("auto");
expect(issueExecutionWorkspaceSettingsSchema.parse({
sharedWorkspaceConcurrency: "allow",
}).sharedWorkspaceConcurrency).toBe("allow");
expect(projectExecutionWorkspacePolicySchema.safeParse({
enabled: true,
sharedWorkspaceConcurrency: "parallel",
}).success).toBe(false);
});
it("accepts an existing-branch pin only with isolated mode and a git_worktree strategy", () => {
expect(issueExecutionWorkspaceSettingsSchema.parse({
mode: "isolated_workspace",
workspaceStrategy: {
type: "git_worktree",
existingBranch: "PAP-14380-salvage-pap-9514",
},
}).workspaceStrategy?.existingBranch).toBe("PAP-14380-salvage-pap-9514");
// Fail closed at the contract layer: an exact-branch pin outside an
// isolated git worktree could silently land in the shared checkout.
expect(issueExecutionWorkspaceSettingsSchema.safeParse({
workspaceStrategy: { type: "git_worktree", existingBranch: "some-branch" },
}).success).toBe(false);
expect(issueExecutionWorkspaceSettingsSchema.safeParse({
mode: "shared_workspace",
workspaceStrategy: { type: "git_worktree", existingBranch: "some-branch" },
}).success).toBe(false);
expect(issueExecutionWorkspaceSettingsSchema.safeParse({
mode: "isolated_workspace",
workspaceStrategy: { type: "project_primary", existingBranch: "some-branch" },
}).success).toBe(false);
expect(issueExecutionWorkspaceSettingsSchema.safeParse({
mode: "isolated_workspace",
workspaceStrategy: {
type: "git_worktree",
existingBranch: "some-branch",
branchTemplate: "{{issue.identifier}}-{{slug}}",
},
}).success).toBe(false);
for (const invalidBranch of ["-leading-dash", "a..b", "has space", "ends/", "back\\slash", "a.lock", "../escape"]) {
expect(issueExecutionWorkspaceSettingsSchema.safeParse({
mode: "isolated_workspace",
workspaceStrategy: { type: "git_worktree", existingBranch: invalidBranch },
}).success).toBe(false);
}
});
it("carries the existing-branch pin through issue settings parsing", () => {
expect(
parseIssueExecutionWorkspaceSettings({
mode: "isolated_workspace",
workspaceStrategy: { type: "git_worktree", existingBranch: " PAP-14754-run-redaction " },
})?.workspaceStrategy,
).toEqual({ type: "git_worktree", existingBranch: "PAP-14754-run-redaction" });
});
it("centralizes unrunnable isolated worktree detection", () => {
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: "isolated_workspace",
resolvedStrategy: "git_worktree",
}),
).toBe(true);
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: "project-1",
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: "isolated_workspace",
resolvedStrategy: "git_worktree",
}),
).toBe(false);
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: "workspace-1",
executionWorkspacePreference: "reuse_existing",
},
resolvedMode: "isolated_workspace",
resolvedStrategy: "git_worktree",
}),
).toBe(false);
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: "shared_workspace",
resolvedStrategy: "git_worktree",
}),
).toBe(false);
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: "agent_default",
resolvedStrategy: "git_worktree",
}),
).toBe(false);
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: "operator_branch",
resolvedStrategy: "git_worktree",
}),
).toBe(true);
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: "isolated_workspace",
resolvedStrategy: "git_worktree",
hasResolvablePriorSessionWorkspace: true,
}),
).toBe(false);
});
it("mirrors runtime default (project_primary) when pinned settings omit strategy type", () => {
// Mode-only pin without explicit workspaceStrategy.type → same project_primary default as runtime.
expect(
resolvePinnedIssueWorkspaceStrategyType({
mode: "isolated_workspace",
issueSettings: { mode: "isolated_workspace" },
}),
).toBe("project_primary");
// Explicit strategy type is always respected.
expect(
resolvePinnedIssueWorkspaceStrategyType({
mode: "isolated_workspace",
issueSettings: {
mode: "isolated_workspace",
workspaceStrategy: { type: "git_worktree" },
},
}),
).toBe("git_worktree");
expect(
resolvePinnedIssueWorkspaceStrategyType({
mode: "isolated_workspace",
issueSettings: {
mode: "isolated_workspace",
workspaceStrategy: { type: "project_primary" },
},
}),
).toBe("project_primary");
});
it("falls back to project policy before legacy project-workspace compatibility flag", () => {
expect(
resolveExecutionWorkspaceMode({
projectPolicy: { enabled: true, defaultMode: "isolated_workspace" },
issueSettings: null,
legacyUseProjectWorkspace: false,
}),
).toBe("isolated_workspace");
expect(
resolveExecutionWorkspaceMode({
projectPolicy: null,
issueSettings: null,
legacyUseProjectWorkspace: false,
}),
).toBe("agent_default");
});
it("applies project policy strategy and runtime defaults when isolation is enabled", () => {
const result = buildExecutionWorkspaceAdapterConfig({
agentConfig: {
workspaceStrategy: { type: "project_primary" },
},
projectPolicy: {
enabled: true,
defaultMode: "isolated_workspace",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/main",
provisionCommand: "bash ./scripts/provision-worktree.sh",
runtimeProvisionCommand: "bash ./scripts/provision-runtime.sh",
},
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev" }],
},
},
issueSettings: null,
mode: "isolated_workspace",
legacyUseProjectWorkspace: null,
});
expect(result.workspaceStrategy).toEqual({
type: "git_worktree",
baseRef: "origin/main",
provisionCommand: "bash ./scripts/provision-worktree.sh",
runtimeProvisionCommand: "bash ./scripts/provision-runtime.sh",
});
expect(result.workspaceRuntime).toEqual({
services: [{ name: "web", command: "pnpm dev" }],
});
});
it("preserves project authorization policy for trust-preset resolution", () => {
expect(parseProjectExecutionWorkspacePolicy({
enabled: true,
authorizationPolicy: {
trustBoundary: {
mode: "low_trust_review",
projectIds: ["33333333-3333-4333-8333-333333333333"],
},
},
})?.authorizationPolicy).toEqual({
trustBoundary: {
mode: "low_trust_review",
projectIds: ["33333333-3333-4333-8333-333333333333"],
},
});
});
it("clears managed workspace strategy when issue opts out to project primary or agent default", () => {
const baseConfig = {
workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}" },
workspaceRuntime: { services: [{ name: "web" }] },
};
expect(
buildExecutionWorkspaceAdapterConfig({
agentConfig: baseConfig,
projectPolicy: { enabled: true, defaultMode: "isolated_workspace" },
issueSettings: { mode: "shared_workspace" },
mode: "shared_workspace",
legacyUseProjectWorkspace: null,
}).workspaceStrategy,
).toBeUndefined();
const agentDefault = buildExecutionWorkspaceAdapterConfig({
agentConfig: baseConfig,
projectPolicy: null,
issueSettings: { mode: "agent_default" },
mode: "agent_default",
legacyUseProjectWorkspace: null,
});
expect(agentDefault.workspaceStrategy).toBeUndefined();
expect(agentDefault.workspaceRuntime).toBeUndefined();
});
it("parses persisted JSON payloads into typed project and issue workspace settings", () => {
expect(
parseProjectExecutionWorkspacePolicy({
enabled: true,
sharedWorkspaceConcurrency: "serialize",
defaultMode: "isolated",
workspaceStrategy: {
type: "git_worktree",
worktreeParentDir: ".paperclip/worktrees",
provisionCommand: "bash ./scripts/provision-worktree.sh",
runtimeProvisionCommand: "bash ./scripts/provision-runtime.sh",
teardownCommand: "bash ./scripts/teardown-worktree.sh",
},
}),
).toEqual({
enabled: true,
sharedWorkspaceConcurrency: "serialize",
defaultMode: "isolated_workspace",
workspaceStrategy: {
type: "git_worktree",
worktreeParentDir: ".paperclip/worktrees",
provisionCommand: "bash ./scripts/provision-worktree.sh",
runtimeProvisionCommand: "bash ./scripts/provision-runtime.sh",
teardownCommand: "bash ./scripts/teardown-worktree.sh",
},
});
expect(
parseIssueExecutionWorkspaceSettings({
mode: "project_primary",
environmentId: "11111111-1111-4111-8111-111111111111",
}),
).toEqual({
mode: "shared_workspace",
});
expect(
parseIssueExecutionWorkspaceSettings(
{
mode: "project_primary",
environmentId: "11111111-1111-4111-8111-111111111111",
},
{ includeEnvironmentId: true },
),
).toEqual({
mode: "shared_workspace",
environmentId: "11111111-1111-4111-8111-111111111111",
});
expect(
parseIssueExecutionWorkspaceSettings({
mode: "isolated_workspace",
sharedWorkspaceConcurrency: "allow",
networkEgress: {
allowFqdns: ["github.com", "pypi.org"],
allowCidrs: ["203.0.113.0/24"],
},
}),
).toEqual({
mode: "isolated_workspace",
sharedWorkspaceConcurrency: "allow",
networkEgress: {
allowFqdns: ["github.com", "pypi.org"],
allowCidrs: ["203.0.113.0/24"],
},
});
});
it("keeps egress grants independent from isolated workspace mode", () => {
const parsedSettings = {
mode: "isolated_workspace" as const,
workspaceRuntime: { image: "example/image" },
networkEgress: {
allowFqdns: ["github.com"],
allowCidrs: ["203.0.113.0/24"],
},
};
expect(selectEnvironmentExecutionWorkspaceSettings(parsedSettings, false)).toEqual({
networkEgress: parsedSettings.networkEgress,
});
expect(selectEnvironmentExecutionWorkspaceSettings(parsedSettings, true)).toEqual(parsedSettings);
expect(selectEnvironmentExecutionWorkspaceSettings({ mode: "isolated_workspace" }, false)).toBeNull();
});
it("prefers the agent default environment", () => {
expect(
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: "agent-env",
instanceDefaultEnvironmentId: "instance-env",
localDefaultEnvironmentId: "local-env",
}),
).toEqual({
environmentId: "agent-env",
source: "agent",
});
});
it("falls back to the instance default environment when the agent has none", () => {
expect(
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: null,
instanceDefaultEnvironmentId: "instance-env",
localDefaultEnvironmentId: "local-env",
}),
).toEqual({
environmentId: "instance-env",
source: "instance",
});
});
it("falls back to the built-in local environment when neither agent nor instance selects one", () => {
expect(
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: null,
instanceDefaultEnvironmentId: null,
localDefaultEnvironmentId: "local-env",
}),
).toEqual({
environmentId: "local-env",
source: "default",
});
});
it("redirects local-landing selections to the managed sandbox under managed-sandbox-only", () => {
// The default fallback and an explicit local selection both land on the
// managed environment; a non-local selection stays untouched.
expect(
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: null,
instanceDefaultEnvironmentId: null,
localDefaultEnvironmentId: "local-env",
managedSandboxOnly: true,
managedSandboxEnvironmentId: "managed-env",
}),
).toEqual({ environmentId: "managed-env", source: "managed" });
expect(
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: "local-env",
instanceDefaultEnvironmentId: null,
localDefaultEnvironmentId: "local-env",
managedSandboxOnly: true,
managedSandboxEnvironmentId: "managed-env",
}),
).toEqual({ environmentId: "managed-env", source: "managed" });
expect(
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: "ssh-env",
instanceDefaultEnvironmentId: null,
localDefaultEnvironmentId: "local-env",
managedSandboxOnly: true,
managedSandboxEnvironmentId: "managed-env",
}),
).toEqual({ environmentId: "ssh-env", source: "agent" });
});
it("fails closed — never local — when managed-sandbox-only has no managed environment", () => {
expect(() =>
resolveExecutionWorkspaceEnvironmentId({
agentDefaultEnvironmentId: null,
instanceDefaultEnvironmentId: null,
localDefaultEnvironmentId: "local-env",
managedSandboxOnly: true,
managedSandboxEnvironmentId: null,
}),
).toThrow(ManagedSandboxUnavailableError);
});
it("maps persisted execution workspace modes back to issue settings", () => {
expect(issueExecutionWorkspaceModeForPersistedWorkspace("isolated_workspace")).toBe("isolated_workspace");
expect(issueExecutionWorkspaceModeForPersistedWorkspace("operator_branch")).toBe("operator_branch");
expect(issueExecutionWorkspaceModeForPersistedWorkspace("shared_workspace")).toBe("shared_workspace");
expect(issueExecutionWorkspaceModeForPersistedWorkspace("adapter_managed")).toBe("agent_default");
expect(issueExecutionWorkspaceModeForPersistedWorkspace("cloud_sandbox")).toBe("agent_default");
expect(issueExecutionWorkspaceModeForPersistedWorkspace(null)).toBe("agent_default");
expect(issueExecutionWorkspaceModeForPersistedWorkspace(undefined)).toBe("agent_default");
});
it("disables project execution workspace policy when the instance flag is off", () => {
expect(
gateProjectExecutionWorkspacePolicy(
{ enabled: true, defaultMode: "isolated_workspace" },
false,
),
).toBeNull();
expect(
gateProjectExecutionWorkspacePolicy(
{ enabled: true, defaultMode: "isolated_workspace" },
true,
),
).toEqual({ enabled: true, defaultMode: "isolated_workspace" });
});
});