diff --git a/server/src/__tests__/heartbeat-session-preparation.test.ts b/server/src/__tests__/heartbeat-session-preparation.test.ts index 9bacb2c7fb1..784b3ff2df4 100644 --- a/server/src/__tests__/heartbeat-session-preparation.test.ts +++ b/server/src/__tests__/heartbeat-session-preparation.test.ts @@ -1,9 +1,10 @@ import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { agents, agentTaskSessions, authUsers, companies, companyMemberships, createDb, heartbeatRuns, issues } from "@paperclipai/db"; +import { agents, agentTaskSessions, authUsers, companies, companyMemberships, createDb, heartbeatRuns, issues, plugins, projects } from "@paperclipai/db"; import { registerServerAdapter, unregisterServerAdapter, type AdapterExecutionContext } from "../adapters/index.js"; import { heartbeatService } from "../services/heartbeat.js"; +import { createPluginWorkspaceFixture } from "./helpers/plugin-workspaces.js"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; const support = await getEmbeddedPostgresTestSupport(); @@ -81,6 +82,29 @@ describePostgres("heartbeat session preparation through adapter dispatch", () => expect(resumed.finished.sessionIdBefore).toBe("saved-session"); }); + it("keeps inherited project policy stable after binding a repository-free workspace", async () => { + const f = await seed(); + const storage = await createPluginWorkspaceFixture(db, f.companyId); + const [plugin] = await db.select().from(plugins).where(eq(plugins.id, storage.pluginId)); + await db.update(plugins).set({ manifestJson: { ...plugin.manifestJson, workspaceProvider: { + ...plugin.manifestJson.workspaceProvider!, defaultForProjects: { mode: "isolated_workspace", strategy: "git_worktree" }, + } } }).where(eq(plugins.id, storage.pluginId)); + const [project] = await db.insert(projects).values({ companyId: f.companyId, name: "Directory project" }).returning(); + await db.update(issues).set({ projectId: project.id }).where(eq(issues.id, f.issueId)); + try { + 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)); + const next = await execute(f); + expect(next.input.runtime.sessionId).toBe("saved-session"); + expect(next.input.config.workspaceStrategy).toEqual(first.input.config.workspaceStrategy); + expect(next.finished.resultJson).toMatchObject({ configFreshness: { + session: { reset: false, taskSessionReused: true, changedCategories: [] }, + workspace: { action: "reuse", workspaceReused: true, changedCategories: [], activeWorkspaceId: bound.executionWorkspaceId }, + } }); + } finally { await db.delete(plugins).where(eq(plugins.id, storage.pluginId)); } + }); + it("starts fresh when effective model configuration changes and persists the reason", async () => { const f = await seed(); await execute(f); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index e6e41be53f3..543d96a97f8 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -2371,7 +2371,7 @@ export function heartbeatService( !boundSourceWorkspace && !taskWorkspaceIntentRow?.intent && !issueContext?.projectWorkspaceId ? taskProjectContext : null ); - const parsedProjectExecutionWorkspacePolicy = workspacePolicyContext && !boundSourceWorkspace + const parsedProjectExecutionWorkspacePolicy = workspacePolicyContext?.hasWorkspace && !boundSourceWorkspace ? await effectiveProjectWorkspacePolicy(db, agent.companyId, workspacePolicyContext.executionWorkspacePolicy, workspacePolicyContext.hasGitSource) : parseProjectExecutionWorkspacePolicy(workspacePolicyContext?.executionWorkspacePolicy); const projectExecutionWorkspacePolicy =