mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
fix(agents): return to the index after termination is accepted (#20667)
Return to the Agents index when termination is accepted, including while asynchronous cleanup continues, so the profile does not remain on an Agent not found error. Cover both action menus, roster refresh, failed requests, and unsaved changes during pending termination. Verified with 53 focused tests, the full UI suite (11,949 passed; one skipped), UI typecheck and build, all applicable CI checks, and Greptile 5/5 with no unresolved comments. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
de7ceac0de
commit
bbf5cfeb62
2 files changed
+32
-8
No files matched your search
@@ -373,18 +373,45 @@ describe("AgentActionButtons", () => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/agents/alpha/runs/run-1");
|
||||
});
|
||||
|
||||
it.each(["terminating", "cleaning_up"] as const)("keeps the detail page open during %s", async lifecycleState => {
|
||||
it.each([
|
||||
["terminating", false],
|
||||
["cleaning_up", false],
|
||||
["terminating", true],
|
||||
["cleaning_up", true],
|
||||
] as const)("leaves the detail page after termination is accepted during %s (work actions in menu: %s)", async (lifecycleState, workActionsInMenu) => {
|
||||
mockAgentsApi.terminate.mockResolvedValue(makeAgent({ status: "terminated", lifecycleState }));
|
||||
const onTerminateSuccess = vi.fn();
|
||||
render(makeAgent(), { onTerminateSuccess, workActionsInMenu });
|
||||
await flushReact();
|
||||
await act(async () => { container.querySelector<HTMLButtonElement>('[aria-label="Open actions for Alpha Agent"]')!.click(); });
|
||||
await flushReact();
|
||||
await act(async () => { Array.from(document.body.querySelectorAll("button")).find(button => button.textContent?.includes("Terminate"))!.click(); });
|
||||
await flushReact();
|
||||
expect(mockAgentsApi.terminate).toHaveBeenCalledWith("agent-1", "company-1");
|
||||
expect(onTerminateSuccess).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: "agent-1",
|
||||
lifecycleState,
|
||||
}));
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "agent-1"] });
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "company-1"] });
|
||||
});
|
||||
|
||||
it("keeps the detail page open and reports a failed termination", async () => {
|
||||
mockAgentsApi.terminate.mockRejectedValue(new Error("Termination failed"));
|
||||
const onTerminateSuccess = vi.fn();
|
||||
render(makeAgent(), { onTerminateSuccess });
|
||||
await flushReact();
|
||||
await act(async () => { container.querySelector<HTMLButtonElement>('[aria-label="Open actions for Alpha Agent"]')!.click(); });
|
||||
await flushReact();
|
||||
await act(async () => { Array.from(document.body.querySelectorAll("button")).find(button => button.textContent?.includes("Terminate"))!.click(); });
|
||||
await flushReact();
|
||||
expect(mockAgentsApi.terminate).toHaveBeenCalledWith("agent-1", "company-1");
|
||||
|
||||
expect(onTerminateSuccess).not.toHaveBeenCalled();
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "agent-1"] });
|
||||
expect(invalidateQueries).not.toHaveBeenCalled();
|
||||
expect(mockPushToast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
body: "Termination failed",
|
||||
tone: "error",
|
||||
}));
|
||||
});
|
||||
|
||||
it("calls the terminate success handler after terminating an agent", async () => {
|
||||
@@ -437,7 +464,7 @@ describe("AgentActionButtons", () => {
|
||||
expect(mockAgentsApi.terminate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rechecks navigation when the form becomes dirty while termination is pending", async () => {
|
||||
it.each([undefined, "terminating", "cleaning_up"] as const)("rechecks unsaved changes before leaving after termination returns %s", async lifecycleState => {
|
||||
let resolveTermination!: (agent: Agent) => void;
|
||||
mockAgentsApi.terminate.mockReturnValue(new Promise((resolve) => {
|
||||
resolveTermination = resolve;
|
||||
@@ -469,7 +496,7 @@ describe("AgentActionButtons", () => {
|
||||
onTerminateSuccess,
|
||||
});
|
||||
await flushReact();
|
||||
resolveTermination(makeAgent({ status: "terminated" }));
|
||||
resolveTermination(makeAgent({ status: "terminated", lifecycleState }));
|
||||
await flushReact();
|
||||
|
||||
expect(onBeforeNavigate).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -216,7 +216,6 @@ export function AgentActionButtons({
|
||||
* omitted, failures surface as toasts (used by the list view).
|
||||
*/
|
||||
onActionError?: (message: string | null) => void;
|
||||
/** Called after termination succeeds so callers can leave now-hidden detail routes. */
|
||||
onTerminateSuccess?: (agent: Agent) => void;
|
||||
/** Extra content rendered just before the overflow menu (e.g. live-run link). */
|
||||
children?: React.ReactNode;
|
||||
@@ -293,8 +292,6 @@ export function AgentActionButtons({
|
||||
onActionError?.(null);
|
||||
invalidateAgent();
|
||||
if (action === "terminate") {
|
||||
const state = (data as Agent).lifecycleState;
|
||||
if (state === "terminating" || state === "cleaning_up") return;
|
||||
if (!confirmLateNavigationChanges(agentActionStartedDirtyRef)) return;
|
||||
onTerminateSuccess?.(data as Agent);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user