diff --git a/server/src/__tests__/issue-closed-workspace-routes.test.ts b/server/src/__tests__/issue-closed-workspace-routes.test.ts index b15070a906..c657ebb006 100644 --- a/server/src/__tests__/issue-closed-workspace-routes.test.ts +++ b/server/src/__tests__/issue-closed-workspace-routes.test.ts @@ -308,6 +308,56 @@ describe.sequential("closed isolated workspace issue routes", () => { expect(mockIssueService.addComment).not.toHaveBeenCalled(); }); + it("accepts a plain comment on a done issue without touching the closed workspace", async () => { + // A comment that does not resume a terminal issue is a pure record: it + // must not rebuild the worktree, and it must not be blocked by a + // workspace it never needed. + mockIssueService.getById.mockResolvedValue({ ...makeIssue(), status: "done", assigneeAgentId: null }); + mockIssueService.addComment.mockResolvedValue({ id: "comment-1", body: "decision recorded" }); + + const res = await request(createApp()) + .post(`/api/issues/${issueId}/comments`) + .send({ body: "decision recorded" }); + + expect(res.status).toBe(201); + expect(mockIssueService.addComment).toHaveBeenCalledTimes(1); + expect(mockExecutionWorkspaceService.reopenClosedIsolatedExecutionWorkspaceForIssue).not.toHaveBeenCalled(); + }); + + it("accepts a plain comment on a done issue even when the workspace cannot be reopened", async () => { + // The audit comment is not resuming anything, so a workspace that is + // un-reopenable must not turn it into a 409. + mockIssueService.getById.mockResolvedValue({ ...makeIssue(), status: "done", assigneeAgentId: null }); + mockIssueService.addComment.mockResolvedValue({ id: "comment-1", body: "decision recorded" }); + mockExecutionWorkspaceService.reopenClosedIsolatedExecutionWorkspaceForIssue.mockResolvedValue({ + ok: false, + code: "not_reopenable", + message: "Execution workspace is not reopenable", + }); + + const res = await request(createApp()) + .post(`/api/issues/${issueId}/comments`) + .send({ body: "decision recorded" }); + + expect(res.status).toBe(201); + expect(mockIssueService.addComment).toHaveBeenCalledTimes(1); + }); + + it("still reopens the closed workspace for a comment that resumes a done issue", async () => { + // `reopen: true` on a terminal issue is a resume: it needs the worktree, + // so the reopen still runs and still blocks when the rebuild fails. + mockIssueService.getById.mockResolvedValue({ ...makeIssue(), status: "done", assigneeAgentId: null }); + mockIssueService.update.mockResolvedValue({ ...makeIssue(), status: "todo" }); + mockIssueService.addComment.mockResolvedValue({ id: "comment-1", body: "hello" }); + + const res = await request(createApp()) + .post(`/api/issues/${issueId}/comments`) + .send({ body: "please continue", reopen: true }); + + expect(mockExecutionWorkspaceService.reopenClosedIsolatedExecutionWorkspaceForIssue).toHaveBeenCalledTimes(1); + expect(res.status).toBe(201); + }); + it("returns 503 and blocks the checkout when the rebuild fails", async () => { mockExecutionWorkspaceService.reopenClosedIsolatedExecutionWorkspaceForIssue.mockResolvedValue({ ok: false, diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 0cc963a4e0..5ba1426b2c 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -17065,9 +17065,14 @@ export function issueRoutes( // blocker, and run-cap gate passes. A rejected comment must not rebuild and // republish the workspace as active, because the issue stays terminal and the // reaper then skips the leaked workspace. + // + // A comment on a terminal issue that does not resume the work is a plain + // record: it never needs the worktree. Rebuilding one for it would either + // churn a workspace the reaper immediately reaps again or block an audit + // comment with a 409 the workspace was never needed for. let reopenedWorkspace: Pick | null = null; let reopenedGeneration: number | null = null; - if (closedExecutionWorkspace) { + if (closedExecutionWorkspace && (!isClosed || effectiveMoveToTodoRequested)) { const reopenOutcome = await reopenClosedIssueExecutionWorkspaceOrRespond( req,