diff --git a/apps/code/src/main/services/archive/service.integration.test.ts b/apps/code/src/main/services/archive/service.integration.test.ts index f5f06d69b..50f7e05b7 100644 --- a/apps/code/src/main/services/archive/service.integration.test.ts +++ b/apps/code/src/main/services/archive/service.integration.test.ts @@ -360,6 +360,20 @@ describe("ArchiveService integration", () => { expect(archived.checkpointId).toBeTruthy(); expect(await pathExists(legacyPath)).toBe(false); })); + + it("archive succeeds when worktree was deleted externally", () => + withTestContext({}, async (ctx) => { + const { worktreePath } = await ctx.setupWorktree("detached"); + + await fs.rm(worktreePath, { recursive: true, force: true }); + expect(await pathExists(worktreePath)).toBe(false); + + const archived = await ctx.service.archiveTask(ctx.archiveInput()); + + expect(archived.checkpointId).toBeNull(); + expect(archived.branchName).toBeNull(); + expect(ctx.archiveRepo.findAll()).toHaveLength(1); + })); }); describe("local/cloud mode", () => { diff --git a/apps/code/src/main/services/archive/service.ts b/apps/code/src/main/services/archive/service.ts index 2a302fe9a..06c98e29a 100644 --- a/apps/code/src/main/services/archive/service.ts +++ b/apps/code/src/main/services/archive/service.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { createGitClient } from "@posthog/git/client"; +import { isGitRepository } from "@posthog/git/queries"; import { CaptureCheckpointSaga, deleteCheckpoint, @@ -173,31 +174,47 @@ export class ArchiveService { if (workspace.mode === "worktree" && worktree) { const worktreePath = worktree.path; - - const actualBranch = await this.getCurrentBranchName(worktreePath); - if (actualBranch && actualBranch !== "HEAD") { - archivedTask.branchName = actualBranch; - } - - await step( - async () => { - if (!archivedTask.checkpointId) { - throw new Error("checkpointId must be set for worktree mode"); - } - await this.captureWorktreeCheckpoint( - folderPath, - worktreePath, - archivedTask.checkpointId, + const worktreeIsValid = await isGitRepository(worktreePath).catch( + (error) => { + log.warn( + `Failed to check worktree at ${worktreePath}; treating as invalid`, + { error }, ); - }, - async () => { - if (archivedTask.checkpointId) { - const git = createGitClient(folderPath); - await deleteCheckpoint(git, archivedTask.checkpointId); - } + return false; }, ); + if (!worktreeIsValid) { + log.warn( + `Worktree at ${worktreePath} is missing or not a git repository; skipping checkpoint capture`, + ); + archivedTask.checkpointId = null; + } else { + const actualBranch = await this.getCurrentBranchName(worktreePath); + if (actualBranch && actualBranch !== "HEAD") { + archivedTask.branchName = actualBranch; + } + + await step( + async () => { + if (!archivedTask.checkpointId) { + throw new Error("checkpointId must be set for worktree mode"); + } + await this.captureWorktreeCheckpoint( + folderPath, + worktreePath, + archivedTask.checkpointId, + ); + }, + async () => { + if (archivedTask.checkpointId) { + const git = createGitClient(folderPath); + await deleteCheckpoint(git, archivedTask.checkpointId); + } + }, + ); + } + await step( async () => { await this.agentService.cancelSessionsByTaskId(taskId);