mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
<!-- 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>
535 lines
19 KiB
TypeScript
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" });
|
|
});
|
|
});
|