mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Surface scheduled retry cancel failures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7593fffb30
commit
133457c2f4
2 files changed
+90
-17
No files matched your search
@@ -635,6 +635,40 @@ describe.sequential("issue comment reopen routes", () => {
|
||||
));
|
||||
});
|
||||
|
||||
it("does not move scheduled-retry issues to todo when POST comment retry cancellation fails", async () => {
|
||||
const issue = {
|
||||
...makeIssue("in_progress"),
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockHeartbeatService.cancelRun.mockRejectedValue(new Error("cancel failed"));
|
||||
|
||||
const res = await request(await installActor(createApp()))
|
||||
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
|
||||
.send({ body: "I added the missing detail; please continue." });
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(mockHeartbeatService.cancelRun).toHaveBeenCalledWith("retry-run-1");
|
||||
expect(mockIssueService.update).not.toHaveBeenCalled();
|
||||
expect(mockIssueService.addComment).not.toHaveBeenCalled();
|
||||
expect(mockLogActivity).not.toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ action: "issue.updated" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps ordinary in-progress POST human comments in progress when no scheduled retry exists", async () => {
|
||||
const issue = makeIssue("in_progress");
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
@@ -874,6 +908,40 @@ describe.sequential("issue comment reopen routes", () => {
|
||||
));
|
||||
});
|
||||
|
||||
it("does not move scheduled-retry issues to todo when PATCH comment retry cancellation fails", async () => {
|
||||
const issue = {
|
||||
...makeIssue("in_progress"),
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockHeartbeatService.cancelRun.mockRejectedValue(new Error("cancel failed"));
|
||||
|
||||
const res = await request(await installActor(createApp()))
|
||||
.patch("/api/issues/11111111-1111-4111-8111-111111111111")
|
||||
.send({ comment: "Retry window is over; please continue." });
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(mockHeartbeatService.cancelRun).toHaveBeenCalledWith("retry-run-1");
|
||||
expect(mockIssueService.update).not.toHaveBeenCalled();
|
||||
expect(mockIssueService.addComment).not.toHaveBeenCalled();
|
||||
expect(mockLogActivity).not.toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ action: "issue.updated" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-assignee agent PATCH comments on closed issues", async () => {
|
||||
mockIssueService.getById.mockResolvedValue(makeIssue("done"));
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
|
||||
+22
-17
@@ -912,11 +912,11 @@ export function issueRoutes(
|
||||
});
|
||||
return cancelledRunId;
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
logger.error(
|
||||
{ err, issueId: input.issue.id, runId: scheduledRetryRunId },
|
||||
"failed to cancel scheduled retry superseded by issue comment",
|
||||
);
|
||||
return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3534,6 +3534,18 @@ export function issueRoutes(
|
||||
) {
|
||||
updateFields.status = "todo";
|
||||
}
|
||||
let cancelledScheduledRetryRunId: string | null = null;
|
||||
if (
|
||||
commentBody &&
|
||||
shouldResumeInProgressScheduledRetry &&
|
||||
updateFields.status === "todo"
|
||||
) {
|
||||
cancelledScheduledRetryRunId = await cancelScheduledRetrySupersededByComment({
|
||||
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
|
||||
issue: existing,
|
||||
actor,
|
||||
});
|
||||
}
|
||||
if (req.body.executionPolicy !== undefined) {
|
||||
updateFields.executionPolicy = applyActorMonitorScheduledBy(
|
||||
normalizeIssueExecutionPolicy(req.body.executionPolicy),
|
||||
@@ -3792,13 +3804,6 @@ export function issueRoutes(
|
||||
previous.status !== undefined &&
|
||||
existing.status === "in_progress" &&
|
||||
issue.status === "todo";
|
||||
const cancelledScheduledRetryRunId = scheduledRetrySupersededByComment
|
||||
? await cancelScheduledRetrySupersededByComment({
|
||||
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
|
||||
issue,
|
||||
actor,
|
||||
})
|
||||
: null;
|
||||
const statusChangedFromBlockedToTodo =
|
||||
existing.status === "blocked" &&
|
||||
issue.status === "todo" &&
|
||||
@@ -5125,22 +5130,22 @@ export function issueRoutes(
|
||||
effectiveMoveToTodoRequested &&
|
||||
(isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers) || shouldResumeInProgressScheduledRetry)
|
||||
) {
|
||||
scheduledRetrySupersededByComment = shouldResumeInProgressScheduledRetry && issue.status === "in_progress";
|
||||
cancelledScheduledRetryRunId = scheduledRetrySupersededByComment
|
||||
? await cancelScheduledRetrySupersededByComment({
|
||||
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
|
||||
issue,
|
||||
actor,
|
||||
})
|
||||
: null;
|
||||
const reopenedIssue = await svc.update(id, { status: "todo" });
|
||||
if (!reopenedIssue) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
}
|
||||
reopened = isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers);
|
||||
scheduledRetrySupersededByComment = shouldResumeInProgressScheduledRetry && issue.status === "in_progress";
|
||||
reopenFromStatus = reopened ? issue.status : null;
|
||||
currentIssue = reopenedIssue;
|
||||
cancelledScheduledRetryRunId = scheduledRetrySupersededByComment
|
||||
? await cancelScheduledRetrySupersededByComment({
|
||||
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
|
||||
issue: currentIssue,
|
||||
actor,
|
||||
})
|
||||
: null;
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: currentIssue.companyId,
|
||||
|
||||
Reference in new issue
Block a user