From 389d22ab6bcf3bf62fe28a92d6256ee3f34a8aca Mon Sep 17 00:00:00 2001 From: Jory Irving Date: Sun, 11 Oct 2026 21:18:26 -0600 Subject: [PATCH] fix(report): restrict already_done to issue implementation (#1229) --- .../[agentName]/tasks/report/route.test.ts | 30 +++++++++++++++++++ .../agents/[agentName]/tasks/report/route.ts | 3 ++ 2 files changed, 33 insertions(+) diff --git a/src/app/api/agents/[agentName]/tasks/report/route.test.ts b/src/app/api/agents/[agentName]/tasks/report/route.test.ts index 629b49a7..920b713c 100644 --- a/src/app/api/agents/[agentName]/tasks/report/route.test.ts +++ b/src/app/api/agents/[agentName]/tasks/report/route.test.ts @@ -1762,6 +1762,20 @@ describe("POST /api/agents/[agentName]/tasks/report — already_done close (#114 }); } + it.each([ + { taskType: "followup-pr", prFixItem: undefined }, + { taskType: "followup-pr", prFixItem: { id: "item-1", generation: 1 } }, + { taskType: "implement", prFixItem: { id: "item-1", generation: 1 } }, + ])("rejects already_done for $taskType with invalid task/token combination before side effects", async ({ taskType, prFixItem }) => { + const res = await postRequest({ ...validBody, taskType, ...(prFixItem ? { prFixItem } : {}) }); + expect(res.status).toBe(400); + expect((await res.json()).error).toMatch(/only valid for implement reports without prFixItem/); + expect(mockAgentRun.create).not.toHaveBeenCalled(); + expect(mockDedupe.create).not.toHaveBeenCalled(); + expect(prFixResolveMock).not.toHaveBeenCalled(); + expect(closeResolveMock).not.toHaveBeenCalled(); + }); + it("returns 400 when already_done has no idempotencyKey", async () => { const { idempotencyKey: _omit, ...body } = validBody; const res = await postRequest(body); @@ -1895,6 +1909,22 @@ describe("POST /api/agents/[agentName]/tasks/report — already_done close (#114 expect(closeResolveMock).toHaveBeenCalledTimes(1); }); + it("settles followup-pr already_addressed with its generation token", async () => { + const res = await postRequest({ + taskType: "followup-pr", + outcome: "already_addressed", + repoFullName: "o/r", + pullRequestNumber: 5, + evidence: "done", + prFixItem: { id: "item-1", generation: 2 }, + }); + expect(res.status).toBe(200); + expect(prFixResolveMock).toHaveBeenCalledWith(expect.objectContaining({ + outcome: "already_addressed", + attempt: { itemId: "item-1", generation: 2 }, + })); + }); + it("does not route already_addressed through the close resolver", async () => { const res = await postRequest({ taskType: "followup-pr", diff --git a/src/app/api/agents/[agentName]/tasks/report/route.ts b/src/app/api/agents/[agentName]/tasks/report/route.ts index 767ba5c2..14c9de76 100644 --- a/src/app/api/agents/[agentName]/tasks/report/route.ts +++ b/src/app/api/agents/[agentName]/tasks/report/route.ts @@ -289,6 +289,9 @@ export async function POST( // stringFields validation. const isAlreadyAddressed = (outcome as ValidOutcome) === "already_addressed"; const isAlreadyDone = (outcome as ValidOutcome) === "already_done"; + if (isAlreadyDone && (taskType !== "implement" || (raw.prFixItem !== undefined && raw.prFixItem !== null))) { + return errorResponse("already_done is only valid for implement reports without prFixItem", 400); + } const evidence = (isAlreadyAddressed || isAlreadyDone) && typeof raw.evidence === "string" ? raw.evidence.trim()