mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
fix live run finalization failure handling
This commit is contained in:
1 parent
0b1cd42614
commit
ddd392812c
2 files changed
+74
-6
No files matched your search
@@ -1502,6 +1502,49 @@ describe("IssueDetail", () => {
|
||||
await flushReact();
|
||||
|
||||
expect(mockIssuesApi.update).toHaveBeenLastCalledWith("PAP-1", { status: "cancelled" });
|
||||
expect(mockHeartbeatsApi.cancel).toHaveBeenCalledTimes(2);
|
||||
expect(mockHeartbeatsApi.cancel.mock.invocationCallOrder[1])
|
||||
.toBeLessThan(mockIssuesApi.update.mock.invocationCallOrder[1]);
|
||||
});
|
||||
|
||||
it("reports partial success when run finalization stops the run but task status update fails", async () => {
|
||||
mockIssuesApi.get.mockResolvedValue(createIssue({
|
||||
status: "in_progress",
|
||||
assigneeAgentId: "agent-1",
|
||||
executionRunId: "run-active-1",
|
||||
}));
|
||||
mockIssuesApi.update.mockRejectedValue(new Error("Status write failed"));
|
||||
mockHeartbeatsApi.cancel.mockResolvedValue(undefined);
|
||||
mockAgentsApi.list.mockResolvedValue([createAgent()]);
|
||||
mockAuthApi.getSession.mockResolvedValue({
|
||||
session: { userId: "user-1" },
|
||||
user: { id: "user-1" },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IssueDetail />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
const stopAndDoneButton = Array.from(container.querySelectorAll("button"))
|
||||
.find((button) => button.textContent?.trim() === "Stop and done");
|
||||
expect(stopAndDoneButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
stopAndDoneButton!.click();
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
expect(mockHeartbeatsApi.cancel).toHaveBeenCalledWith("run-active-1");
|
||||
expect(mockPushToast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: "Run stopped; task update failed",
|
||||
body: "Run was stopped, but updating the task failed: Status write failed",
|
||||
tone: "error",
|
||||
}));
|
||||
});
|
||||
|
||||
it("passes planning work mode to the issue chat thread", async () => {
|
||||
|
||||
@@ -166,6 +166,24 @@ import {
|
||||
type IssueTreeControlMode,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
type StopAndFinalizeRunError = Error & {
|
||||
runCancelledBeforeStatusUpdateFailed?: boolean;
|
||||
};
|
||||
|
||||
function createRunCancelledStatusUpdateError(err: unknown): StopAndFinalizeRunError {
|
||||
const message = err instanceof Error
|
||||
? `Run was stopped, but updating the task failed: ${err.message}`
|
||||
: "Run was stopped, but updating the task failed. Retry the task status update.";
|
||||
const error = new Error(message) as StopAndFinalizeRunError;
|
||||
error.runCancelledBeforeStatusUpdateFailed = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
function didRunCancelBeforeStatusUpdateFail(err: unknown): err is StopAndFinalizeRunError {
|
||||
return err instanceof Error &&
|
||||
(err as StopAndFinalizeRunError).runCancelledBeforeStatusUpdateFailed === true;
|
||||
}
|
||||
|
||||
type CommentReassignment = IssueCommentReassignment;
|
||||
type ActionableIssueThreadInteraction =
|
||||
| SuggestTasksInteraction
|
||||
@@ -1978,7 +1996,11 @@ export function IssueDetail() {
|
||||
const stopAndFinalizeRun = useMutation({
|
||||
mutationFn: async ({ runId, status }: { runId: string; status: "cancelled" | "done" }) => {
|
||||
await heartbeatsApi.cancel(runId);
|
||||
return issuesApi.update(issueId!, { status });
|
||||
try {
|
||||
return await issuesApi.update(issueId!, { status });
|
||||
} catch (err) {
|
||||
throw createRunCancelledStatusUpdateError(err);
|
||||
}
|
||||
},
|
||||
onSuccess: ({ comment: _comment, ...nextIssue }, { status }) => {
|
||||
const issueRefs = new Set<string>([issueId!, nextIssue.id]);
|
||||
@@ -1993,15 +2015,18 @@ export function IssueDetail() {
|
||||
});
|
||||
},
|
||||
onError: (err, { status }) => {
|
||||
const runWasStopped = didRunCancelBeforeStatusUpdateFail(err);
|
||||
pushToast({
|
||||
title: status === "done" ? "Stop and done failed" : "Stop and cancel failed",
|
||||
title: runWasStopped
|
||||
? "Run stopped; task update failed"
|
||||
: status === "done" ? "Stop and done failed" : "Stop and cancel failed",
|
||||
body: err instanceof Error ? err.message : "Unable to stop the run and update the task",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
onSettled: (_data, err) => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
||||
invalidateIssueRunState();
|
||||
if (err) invalidateIssueRunState();
|
||||
if (selectedCompanyId) {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
||||
}
|
||||
@@ -3136,7 +3161,7 @@ export function IssueDetail() {
|
||||
stopAndFinalizeRun.variables?.status === "cancelled",
|
||||
disabled: stopAndFinalizeRun.isPending,
|
||||
onSelect: (runId) =>
|
||||
stopAndFinalizeRun.mutateAsync({ runId, status: "cancelled" }).then(() => undefined),
|
||||
stopAndFinalizeRun.mutateAsync({ runId, status: "cancelled" }).then(() => undefined, () => undefined),
|
||||
},
|
||||
{
|
||||
id: "done",
|
||||
@@ -3147,7 +3172,7 @@ export function IssueDetail() {
|
||||
stopAndFinalizeRun.variables?.status === "done",
|
||||
disabled: stopAndFinalizeRun.isPending,
|
||||
onSelect: (runId) =>
|
||||
stopAndFinalizeRun.mutateAsync({ runId, status: "done" }).then(() => undefined),
|
||||
stopAndFinalizeRun.mutateAsync({ runId, status: "done" }).then(() => undefined, () => undefined),
|
||||
},
|
||||
], [
|
||||
stopAndFinalizeRun.isPending,
|
||||
|
||||
Reference in new issue
Block a user