diff --git a/server/src/__tests__/workspace-cleanup-retained-local-dir.test.ts b/server/src/__tests__/workspace-cleanup-retained-local-dir.test.ts new file mode 100644 index 00000000000..eb417517e60 --- /dev/null +++ b/server/src/__tests__/workspace-cleanup-retained-local-dir.test.ts @@ -0,0 +1,33 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { cleanupExecutionWorkspaceArtifacts } from "../services/workspace-runtime.ts"; + +const dirs: string[] = []; +afterEach(async () => { await Promise.all(dirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true }))); }); + +async function localWorkspace(createdByRuntime: boolean) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-retained-dir-")); dirs.push(root); + const cwd = path.join(root, "task-directory"); + await fs.mkdir(cwd); await fs.writeFile(path.join(cwd, "website-fixture.txt"), "saved\n"); + return { cwd, workspace: { id: "workspace-1", companyId: "company-1", cwd, providerType: "local_fs", providerRef: null, + branchName: null, repoUrl: null, baseRef: null, projectId: null, projectWorkspaceId: null, sourceIssueId: "issue-1", + metadata: { createdByRuntime } } }; +} + +describe("closing a local workspace directory", () => { + it("reports a retained directory the runtime did not create as cleaned and keeps its files", async () => { + const { cwd, workspace } = await localWorkspace(false); + const cleanup = await cleanupExecutionWorkspaceArtifacts({ workspace }); + expect(cleanup).toMatchObject({ cleaned: true, warnings: [] }); + await expect(fs.readFile(path.join(cwd, "website-fixture.txt"), "utf8")).resolves.toBe("saved\n"); + }); + + it("still removes a runtime-created directory", async () => { + const { cwd, workspace } = await localWorkspace(true); + const cleanup = await cleanupExecutionWorkspaceArtifacts({ workspace }); + expect(cleanup.cleaned).toBe(true); + await expect(fs.stat(cwd)).rejects.toThrow(); + }); +}); diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index fc7196c75b7..518f0898dfc 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -4590,8 +4590,13 @@ export async function cleanupExecutionWorkspaceArtifacts(input: { } } + // A local directory the runtime did not create is never removed here (operator-owned or + // retained, such as an isolated task directory). Closing such a record has nothing left to + // clean, so its surviving directory is the intended outcome, not a failed cleanup. + const retainedByDesign = input.workspace.providerType === "local_fs" && !createdByRuntime; const cleaned = !workspacePath || + retainedByDesign || !(await directoryExists(workspacePath)); if (cleaned) warnings.push(...await releaseNestedRepositoryWorktrees(nestedWorktrees));