fix human reopen with pending finalize blockers

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-31 23:30:38 +00:00
1 parent f51cba33fa
commit 402f0bf0a2
2 files changed
+99 -2

No files matched your search

@@ -325,6 +325,7 @@ describe.sequential("issue comment reopen routes", () => {
blockerIssueIds: [],
unresolvedBlockerIssueIds: [],
unresolvedBlockerCount: 0,
pendingFinalizeBlockerIssueIds: [],
allBlockersDone: true,
isDependencyReady: true,
});
@@ -853,6 +854,60 @@ describe.sequential("issue comment reopen routes", () => {
));
});
it("moves blocked issues with only pending-finalize done blockers back to todo via POST human comments", async () => {
mockIssueService.getById.mockResolvedValue(makeIssue("blocked"));
mockIssueService.getDependencyReadiness.mockResolvedValue({
issueId: "11111111-1111-4111-8111-111111111111",
blockerIssueIds: ["33333333-3333-4333-8333-333333333333"],
unresolvedBlockerIssueIds: ["33333333-3333-4333-8333-333333333333"],
unresolvedBlockerCount: 1,
pendingFinalizeBlockerIssueIds: ["33333333-3333-4333-8333-333333333333"],
allBlockersDone: false,
isDependencyReady: false,
});
mockIssueService.update.mockImplementation(async (_id: string, patch: Record<string, unknown>) => ({
...makeIssue("blocked"),
...patch,
}));
const res = await request(await installActor(createApp()))
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
.send({ body: "please continue while finalization recovers" });
expect(res.status).toBe(201);
expect(mockIssueService.update).toHaveBeenCalledWith(
"11111111-1111-4111-8111-111111111111",
{ status: "todo" },
);
await waitForWakeup(() => expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(
"22222222-2222-4222-8222-222222222222",
expect.objectContaining({ reason: "issue_reopened_via_comment" }),
));
});
it("does not move assigned blocked issues to todo via POST agent comments", async () => {
const assigneeAgentId = "22222222-2222-4222-8222-222222222222";
mockIssueService.getById.mockResolvedValue(makeIssue("blocked"));
mockIssueService.addComment.mockResolvedValue({
id: "comment-1",
issueId: "11111111-1111-4111-8111-111111111111",
companyId: "company-1",
body: "agent progress note",
createdAt: new Date(),
updatedAt: new Date(),
authorAgentId: assigneeAgentId,
authorUserId: null,
});
const res = await request(await installActor(createApp(), agentActor(assigneeAgentId)))
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
.send({ body: "agent progress note" });
expect(res.status).toBe(201);
expect(mockIssueService.update).not.toHaveBeenCalled();
expect(mockIssueService.getDependencyReadiness).not.toHaveBeenCalled();
});
it("moves in-progress issues with a scheduled retry back to todo via POST human comments", async () => {
const issue = {
...makeIssue("in_progress"),
@@ -1329,6 +1384,41 @@ describe.sequential("issue comment reopen routes", () => {
));
});
it("moves blocked issues with only pending-finalize done blockers back to todo via PATCH human comments", async () => {
mockIssueService.getById.mockResolvedValue(makeIssue("blocked"));
mockIssueService.getDependencyReadiness.mockResolvedValue({
issueId: "11111111-1111-4111-8111-111111111111",
blockerIssueIds: ["33333333-3333-4333-8333-333333333333"],
unresolvedBlockerIssueIds: ["33333333-3333-4333-8333-333333333333"],
unresolvedBlockerCount: 1,
pendingFinalizeBlockerIssueIds: ["33333333-3333-4333-8333-333333333333"],
allBlockersDone: false,
isDependencyReady: false,
});
mockIssueService.update.mockImplementation(async (_id: string, patch: Record<string, unknown>) => ({
...makeIssue("blocked"),
...patch,
}));
const res = await request(await installActor(createApp()))
.patch("/api/issues/11111111-1111-4111-8111-111111111111")
.send({ comment: "please continue while finalization recovers" });
expect(res.status).toBe(200);
expect(mockIssueService.update).toHaveBeenCalledWith(
"11111111-1111-4111-8111-111111111111",
expect.objectContaining({
status: "todo",
actorAgentId: null,
actorUserId: "local-board",
}),
);
await waitForWakeup(() => expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(
"22222222-2222-4222-8222-222222222222",
expect.objectContaining({ reason: "issue_reopened_via_comment" }),
));
});
it("moves in-progress issues with a scheduled retry back to todo via the PATCH comment path", async () => {
const issue = {
...makeIssue("in_progress"),
+9 -2
View File
@@ -1771,6 +1771,13 @@ function shouldHumanCommentResumeInProgressScheduledRetry(input: {
return typeof input.assigneeAgentId === "string" && input.assigneeAgentId.length > 0;
}
function hasUnresolvedBlockerEdges(readiness: {
unresolvedBlockerCount: number;
pendingFinalizeBlockerIssueIds?: string[];
}) {
return readiness.unresolvedBlockerCount > (readiness.pendingFinalizeBlockerIssueIds?.length ?? 0);
}
function isExplicitResumeCapableStatus(status: string | null | undefined) {
return status === "done" || status === "blocked" || status === "todo" || status === "in_progress";
}
@@ -7859,7 +7866,7 @@ export function issueRoutes(
: null;
const hasUnresolvedFirstClassBlockers =
isBlocked && effectiveMoveToTodoRequested
? (await svc.getDependencyReadiness(existing.id)).unresolvedBlockerCount > 0
? hasUnresolvedBlockerEdges(await svc.getDependencyReadiness(existing.id))
: false;
if (resumeRequested === true && isBlocked && hasUnresolvedFirstClassBlockers) {
res.status(409).json({ error: "Issue follow-up blocked by unresolved blockers" });
@@ -10097,7 +10104,7 @@ export function issueRoutes(
shouldResumeInProgressScheduledRetry);
const hasUnresolvedFirstClassBlockers =
isBlocked && effectiveMoveToTodoRequested
? (await svc.getDependencyReadiness(issue.id)).unresolvedBlockerCount > 0
? hasUnresolvedBlockerEdges(await svc.getDependencyReadiness(issue.id))
: false;
if (resumeRequested === true && isBlocked && hasUnresolvedFirstClassBlockers) {
res.status(409).json({ error: "Issue follow-up blocked by unresolved blockers" });