fix(server): keep issue environment parsing opt-in

Keep normal issue workspace settings normalization from persisting legacy environmentId values while allowing accepted-plan pre-realization and cleanup paths to inspect existing legacy environment intent.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-07-08 16:38:23 +00:00
1 parent b63a24636e
commit 294f4c8332
4 files changed
+99 -4

No files matched your search

@@ -155,10 +155,23 @@ describe("execution workspace policy helpers", () => {
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",
});
});
it("prefers the agent default environment", () => {
@@ -3084,6 +3084,78 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => {
]);
});
it("createChild preserves strategy-only workspace intent without realizing the parent workspace", async () => {
const companyId = randomUUID();
const projectId = randomUUID();
const parentIssueId = randomUUID();
const projectWorkspaceId = randomUUID();
const environmentId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
await db.insert(projects).values({
id: projectId,
companyId,
name: "Workspace project",
status: "in_progress",
});
await db.insert(projectWorkspaces).values({
id: projectWorkspaceId,
companyId,
projectId,
name: "Primary workspace",
isPrimary: true,
});
await db.insert(issues).values({
id: parentIssueId,
companyId,
projectId,
projectWorkspaceId,
title: "Accepted plan parent",
status: "in_progress",
priority: "medium",
executionWorkspaceSettings: {
mode: "isolated_workspace",
environmentId,
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/master",
branchTemplate: "{{issue.identifier}}-{{slug}}",
},
},
});
const { issue: child } = await svc.createChild(parentIssueId, {
title: "Accepted plan child",
status: "todo",
priority: "medium",
executionWorkspaceInheritanceMode: "strategy_only",
});
expect(child.parentId).toBe(parentIssueId);
expect(child.projectId).toBe(projectId);
expect(child.projectWorkspaceId).toBe(projectWorkspaceId);
expect(child.executionWorkspaceId).toBeNull();
expect(child.executionWorkspacePreference).toBeNull();
expect(child.executionWorkspaceSettings).toEqual({
mode: "isolated_workspace",
environmentId,
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/master",
branchTemplate: "{{issue.identifier}}-{{slug}}",
},
});
});
it("clamps helper-created child requestDepth to the safe maximum", async () => {
const companyId = randomUUID();
const projectId = randomUUID();
@@ -88,7 +88,14 @@ export function gateProjectExecutionWorkspacePolicy(
return projectPolicy;
}
export function parseIssueExecutionWorkspaceSettings(raw: unknown): IssueExecutionWorkspaceSettings | null {
type ParseIssueExecutionWorkspaceSettingsOptions = {
includeEnvironmentId?: boolean;
};
export function parseIssueExecutionWorkspaceSettings(
raw: unknown,
options: ParseIssueExecutionWorkspaceSettingsOptions = {},
): IssueExecutionWorkspaceSettings | null {
const parsed = parseObject(raw);
if (Object.keys(parsed).length === 0) return null;
const workspaceStrategy = parseExecutionWorkspaceStrategy(parsed.workspaceStrategy);
@@ -112,7 +119,7 @@ export function parseIssueExecutionWorkspaceSettings(raw: unknown): IssueExecuti
...(normalizedMode
? { mode: normalizedMode as IssueExecutionWorkspaceSettings["mode"] }
: {}),
...(parsed.environmentId && typeof parsed.environmentId === "string"
...(options.includeEnvironmentId && (typeof parsed.environmentId === "string" || parsed.environmentId === null)
? { environmentId: parsed.environmentId }
: {}),
...(workspaceStrategy ? { workspaceStrategy } : {}),
+5 -2
View File
@@ -241,7 +241,7 @@ function buildReusedExecutionWorkspaceConfigPatchFromIssueSettings(
// Accepted-plan children are not realized yet, so carry only unresolved
// workspace intent and let the first child run render/persist its own branch.
function buildPreRealizationExecutionWorkspaceSettings(raw: unknown): Record<string, unknown> | null {
const settings = parseIssueExecutionWorkspaceSettings(raw);
const settings = parseIssueExecutionWorkspaceSettings(raw, { includeEnvironmentId: true });
if (!settings) return null;
const mode =
settings.mode && settings.mode !== "inherit" && settings.mode !== "reuse_existing"
@@ -6409,7 +6409,10 @@ export function issueService(db: Db) {
let cleared = 0;
for (const row of rows) {
const settings = parseIssueExecutionWorkspaceSettings(row.executionWorkspaceSettings);
const settings = parseIssueExecutionWorkspaceSettings(
row.executionWorkspaceSettings,
{ includeEnvironmentId: true },
);
if (settings?.environmentId !== environmentId) continue;
await db