diff --git a/maps/feature-map/runs-adapters.md b/maps/feature-map/runs-adapters.md index 9432c9a792b..25744903964 100644 --- a/maps/feature-map/runs-adapters.md +++ b/maps/feature-map/runs-adapters.md @@ -75,7 +75,9 @@ mutations. Qualify a useful model task separately from saving the account. ### `session` For a repository project that inherits storage-plugin defaults, run a task and -send two follow-ups. Binding the workspace must preserve the effective policy, +send two follow-ups. Start with unset task workspace settings, then repeat with +an explicit isolated mode or strategy. Binding must save `reuse_existing` and +the resolved mode without changing the effective policy, session fingerprint, and workspace identity. An actual project-policy change must still reset the session and preserve the stored workspace owner. The [session admission regression](../../server/src/__tests__/heartbeat-session-preparation.test.ts) diff --git a/server/src/__tests__/heartbeat-session-preparation.test.ts b/server/src/__tests__/heartbeat-session-preparation.test.ts index 13d0544016b..2d117a6855c 100644 --- a/server/src/__tests__/heartbeat-session-preparation.test.ts +++ b/server/src/__tests__/heartbeat-session-preparation.test.ts @@ -112,7 +112,11 @@ describePostgres("heartbeat session preparation through adapter dispatch", () => } finally { await db.delete(plugins).where(eq(plugins.id, storage.pluginId)); } }); - it("keeps inherited repository policy stable through binding and retains the provider on policy changes", async () => { + it.each([ + null, + { mode: "isolated_workspace" }, + { workspaceStrategy: { type: "git_worktree", branchTemplate: "work" } }, + ])("keeps inherited repository policy stable through binding with settings %j and retains the provider on policy changes", async (settings) => { const f = await seed(); const storage = await createPluginWorkspaceFixture(db, f.companyId); const [plugin] = await db.select().from(plugins).where(eq(plugins.id, storage.pluginId)); @@ -121,7 +125,7 @@ describePostgres("heartbeat session preparation through adapter dispatch", () => } } }).where(eq(plugins.id, storage.pluginId)); await db.update(projects).set({ executionWorkspacePolicy: null }).where(eq(projects.id, storage.input.projectId)); await db.update(issues).set({ projectId: storage.input.projectId, projectWorkspaceId: storage.input.projectWorkspaceId, - executionWorkspaceSettings: { workspaceStrategy: { type: "git_worktree", branchTemplate: "work" } }, + executionWorkspaceSettings: settings, }).where(eq(issues.id, f.issueId)); await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true, enableIsolatedWorkspacesByDefault: true }); const cwd = await mkdtemp(join(tmpdir(), "session-provider-fixture-")); @@ -140,6 +144,8 @@ describePostgres("heartbeat session preparation through adapter dispatch", () => const first = await execute(f); const [bound] = await db.select().from(issues).where(eq(issues.id, f.issueId)); expect(bound.executionWorkspaceId).toEqual(expect.any(String)); + expect(bound.executionWorkspacePreference).toBe("reuse_existing"); + expect(bound.executionWorkspaceSettings).toEqual({ ...settings, mode: "isolated_workspace" }); const [workspace] = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, bound.executionWorkspaceId!)); expect([...captured.values()].filter(input => input.context.issueId === f.issueId)).toHaveLength(1); expect(workspace).toMatchObject({ providerType: "plugin", strategyType: "git_worktree" }); diff --git a/server/src/__tests__/helpers/plugin-workspaces.ts b/server/src/__tests__/helpers/plugin-workspaces.ts index 31cc69c1546..0ebb4af7a03 100644 --- a/server/src/__tests__/helpers/plugin-workspaces.ts +++ b/server/src/__tests__/helpers/plugin-workspaces.ts @@ -100,6 +100,7 @@ export async function createPluginWorkspaceFixture( status: "running", }); const calls: any[] = []; + const preparedBranches = new Map(); let loseResponse = false, dirty = false, removed = false, @@ -120,6 +121,7 @@ export async function createPluginWorkspaceFixture( } if (params.operation.kind === "prepare") { removed = false; + preparedBranches.set(params.executionWorkspaceId, params.operation.branchName ?? null); return { kind: "prepared", binding: { @@ -141,7 +143,7 @@ export async function createPluginWorkspaceFixture( return { kind: "inspection", headSha: "a".repeat(40), - branchName: "work", + branchName: preparedBranches.get(params.executionWorkspaceId) ?? "work", hasDirtyTrackedFiles: dirty, hasUntrackedFiles: false, hasLocalOnlyData: false,