Files
PaperClipAI/server/src/__tests__/execution-workspace-policy.test.ts
T
Devin FoleyandPaperclip ea371b9684 fix: retain project defaults in partial workspace overrides (#14502)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Project workspace policies define how isolated worktrees are set up.
> - Tasks can override a branch without providing every setup field.
> - The resolver currently replaces the entire project strategy with
that partial override.
> - Losing an explicit setup command can run the repository fallback
script and block the task.
> - This pull request keeps enabled project defaults when the task uses
the same strategy type.

## Linked Issues or Issue Description

**What happened?**
A project uses `git_worktree` with `provisionCommand: "true"`. A task
overrides only `baseRef`. The resolver drops the command. Worktree
creation then invokes `scripts/provision-worktree.sh`, which can fail
because its required setup is absent.

**Expected behavior**
A branch override keeps the project's provision, runtime provision, and
teardown commands unless the task explicitly overrides them. A different
strategy type must not inherit those commands.

**Steps to reproduce**
Configure the project with an enabled `git_worktree` strategy and
`provisionCommand: "true"`. Give the task an isolated workspace with a
`git_worktree` strategy and a different `baseRef`. Add a failing
repository fallback provisioner. Before this change, worktree creation
invokes that script. After this change, it uses the project's explicit
command and succeeds.

Related: #4968 concerns agent strategy and working-directory fallback.
#13903 concerns gated API fields and reusable-workspace updates. #11091
concerns provision hooks on workspace reuse. None fixes partial task
overrides discarding project defaults.

## What Changed

- Merge a partial task strategy over the enabled project's strategy only
when their types match.
- Preserve explicit null values when parsing nullable strategy fields,
so they can clear project values.
- Keep explicit empty-string overrides and agent fallback behavior.
- Exclude disabled project strategies and avoid an inherited branch
template when a task pins an existing branch.
- Add policy regression coverage and a real Git worktree test with a
failing fallback script.
- Document inheritance, explicit clearing, and no-op provisioning in the
development guide.

## Verification

- Policy regression: eight failures before the fix; all 41 policy tests
pass after it.
- Real worktree regression: passes and creates a worktree using the
task's base branch without invoking the failing fallback provisioner.
- `pnpm -r typecheck`: passed.
- `pnpm build`: passed.
- `pnpm test:run`: general-server phase completed with 13,873 passed, 86
skipped, and 14 failures in unchanged macOS skills-cache and Git
long-path tests. The same failures reproduce on unmodified base code.
The command stops at that phase, so no full local pass is claimed.
- CI initially failed the existing Telegram subscription recovery test
on a 15-second timeout. The separate fix and investigation are in
#14501. A serialized job also lost its runner; GitHub reported lost
communication, and that job was rerun without source changes. All 52
final-commit checks pass, with two intentional skips. Greptile is 5/5,
with no unresolved comments or merge conflicts. The chat shard passed on
one unchanged rerun. The timeout cause remains unproven; #14501 adds
phase diagnostics for a recurrence.

## Risks

Tasks that specify a partial strategy now retain the project's omitted
fields, including setup and teardown hooks. This is the intended
behavior change. Inheritance requires an enabled project policy and
matching strategy types. Explicit task values still win. Null and empty
commands restore existing runtime defaults; they do not guarantee that
no script runs. Use `"true"` for an explicit no-op provision command. No
migration, live configuration change, or task replay is included.

## Model Used

OpenAI GPT-6 (Codex), with reasoning, terminal tools, and code
execution. The context window size is not exposed in this session.

## 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 references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run focused tests locally and they pass; full-suite status
is recorded above
- [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>
2026-09-28 18:09:20 -07:00

760 lines
26 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
issueExecutionWorkspaceSettingsSchema,
projectExecutionWorkspacePolicySchema,
} from "@paperclipai/shared";
import {
applyDefaultIsolatedExecutionWorkspacePolicy,
buildExecutionWorkspaceAdapterConfig,
defaultIssueExecutionWorkspaceSettingsForProject,
gateProjectExecutionWorkspacePolicy,
isUnrunnableWorktreeCombo,
issueExecutionWorkspaceModeForPersistedWorkspace,
parseIssueExecutionWorkspaceSettings,
parseProjectExecutionWorkspacePolicy,
ManagedSandboxUnavailableError,
resolveEffectiveWorkspaceStrategyType,
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" }],
});
});
describe("partial issue workspace strategies", () => {
const projectStrategy = {
type: "git_worktree" as const,
baseRef: "origin/main",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
provisionCommand: "true",
runtimeProvisionCommand: "npm run setup:runtime",
teardownCommand: "npm run teardown",
};
function resolveStrategy(
strategy: Record<string, unknown>,
enabled = true,
) {
return buildExecutionWorkspaceAdapterConfig({
agentConfig: { workspaceStrategy: { type: "git_worktree", provisionCommand: "agent-setup" } },
projectPolicy: parseProjectExecutionWorkspacePolicy({
enabled,
defaultMode: "isolated_workspace",
workspaceStrategy: projectStrategy,
}),
issueSettings: parseIssueExecutionWorkspaceSettings({
mode: "isolated_workspace",
workspaceStrategy: strategy,
}),
mode: "isolated_workspace",
legacyUseProjectWorkspace: null,
}).workspaceStrategy;
}
it("retains project hooks when an issue changes only its base branch", () => {
expect(resolveStrategy({ type: "git_worktree", baseRef: "origin/release" })).toEqual({
...projectStrategy,
baseRef: "origin/release",
});
});
it.each(["npm run issue-setup", "", null])("honors an explicit provisioning override of %j", (provisionCommand) => {
expect(resolveStrategy({ type: "git_worktree", provisionCommand })).toEqual({
...projectStrategy,
provisionCommand,
});
});
it("preserves explicit null clears through persisted JSON parsing", () => {
const strategy = {
type: "git_worktree",
baseRef: null,
branchTemplate: null,
worktreeParentDir: null,
provisionCommand: null,
runtimeProvisionCommand: null,
teardownCommand: null,
};
expect(resolveStrategy(strategy)).toEqual(strategy);
});
it.each(["cloud_sandbox", "adapter_managed", "project_primary"])("does not carry project hooks into %s", (type) => {
expect(resolveStrategy({ type })).toEqual({ type });
});
it("does not inherit a disabled project strategy", () => {
expect(resolveStrategy({ type: "git_worktree", baseRef: "origin/release" }, false)).toEqual({
type: "git_worktree",
baseRef: "origin/release",
});
expect(resolveStrategy({}, false)).toEqual({
type: "git_worktree",
provisionCommand: "agent-setup",
});
});
it("keeps project hooks for an exact branch pin without inheriting a branch template", () => {
const resolved = resolveStrategy({ type: "git_worktree", existingBranch: "fix/existing" });
expect(resolved).toEqual({
...projectStrategy,
branchTemplate: undefined,
existingBranch: "fix/existing",
});
expect(issueExecutionWorkspaceSettingsSchema.safeParse({
mode: "isolated_workspace",
workspaceStrategy: resolved,
}).success).toBe(true);
});
it("does not mutate the project or issue strategy", () => {
const issueStrategy = { type: "git_worktree" as const, baseRef: "origin/release" };
const result = buildExecutionWorkspaceAdapterConfig({
agentConfig: {},
projectPolicy: { enabled: true, workspaceStrategy: Object.freeze({ ...projectStrategy }) },
issueSettings: { workspaceStrategy: Object.freeze(issueStrategy) },
mode: "isolated_workspace",
legacyUseProjectWorkspace: null,
});
expect(result.workspaceStrategy).not.toBe(issueStrategy);
expect(issueStrategy).toEqual({ type: "git_worktree", baseRef: "origin/release" });
expect(projectStrategy.baseRef).toBe("origin/main");
});
});
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" });
});
});
describe("operator default isolated execution workspaces", () => {
const withDefault = (
projectPolicy: Parameters<
typeof applyDefaultIsolatedExecutionWorkspacePolicy
>[0]["projectPolicy"],
hasProjectWorkspace = true,
defaultIsolatedWorkspacesEnabled = true,
) =>
applyDefaultIsolatedExecutionWorkspacePolicy({
projectPolicy,
defaultIsolatedWorkspacesEnabled,
hasProjectWorkspace,
});
it("substitutes an isolated policy for a project that stores none", () => {
expect(withDefault(null)).toEqual({
enabled: true,
defaultMode: "isolated_workspace",
});
});
it("leaves everything alone while the operator default is off", () => {
expect(withDefault(null, true, false)).toBeNull();
});
it("keeps a task that has no project on its existing behavior", () => {
// Isolation needs a repository to cut a worktree from. A project-less task
// (agent chat, for example) must not be pulled into worktree mode.
expect(withDefault(null, false)).toBeNull();
});
it("keeps a project without a configured workspace on its existing behavior", () => {
const projectPolicy = withDefault(null, false);
expect(projectPolicy).toBeNull();
expect(resolveExecutionWorkspaceMode({
projectPolicy,
issueSettings: null,
legacyUseProjectWorkspace: null,
})).toBe("shared_workspace");
expect(withDefault({ enabled: true, defaultMode: "isolated_workspace" }, false))
.toEqual({ enabled: true, defaultMode: "isolated_workspace" });
});
it("never overrides a policy the project already stores", () => {
expect(withDefault({ enabled: true, defaultMode: "shared_workspace" })).toEqual({
enabled: true,
defaultMode: "shared_workspace",
});
// `enabled: false` is a tenant decision to stay on the shared checkout,
// not an absent policy to fill in.
expect(withDefault({ enabled: false })).toEqual({ enabled: false });
});
it("resolves an unpolicied project's tasks to an isolated workspace", () => {
expect(
resolveExecutionWorkspaceMode({
projectPolicy: withDefault(null),
issueSettings: null,
legacyUseProjectWorkspace: null,
}),
).toBe("isolated_workspace");
});
it("still lets an explicit issue setting win over the operator default", () => {
expect(
resolveExecutionWorkspaceMode({
projectPolicy: withDefault(null),
issueSettings: { mode: "shared_workspace" },
legacyUseProjectWorkspace: null,
}),
).toBe("shared_workspace");
});
it("keeps mode and strategy coherent for the substituted policy", () => {
// Substituting a policy (rather than moving the terminal fallback) is what
// makes `hasWorkspaceControl` true, so the default git_worktree strategy is
// supplied instead of leaving isolated mode on a project_primary strategy.
const projectPolicy = withDefault(null);
const mode = resolveExecutionWorkspaceMode({
projectPolicy,
issueSettings: null,
legacyUseProjectWorkspace: null,
});
const config = buildExecutionWorkspaceAdapterConfig({
agentConfig: {},
projectPolicy,
issueSettings: null,
mode,
legacyUseProjectWorkspace: null,
});
expect(resolveEffectiveWorkspaceStrategyType(mode, config)).toBe("git_worktree");
});
it("does not strand a project-less task as an unrunnable worktree", () => {
const projectPolicy = withDefault(null, false);
const mode = resolveExecutionWorkspaceMode({
projectPolicy,
issueSettings: null,
legacyUseProjectWorkspace: null,
});
const config = buildExecutionWorkspaceAdapterConfig({
agentConfig: {},
projectPolicy,
issueSettings: null,
mode,
legacyUseProjectWorkspace: null,
});
expect(
isUnrunnableWorktreeCombo({
issue: {
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
},
resolvedMode: mode,
resolvedStrategy: resolveEffectiveWorkspaceStrategyType(mode, config),
}),
).toBe(false);
});
});