From 152edab8d0fbb94d026a4eb8559b60d14be19ca8 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:02:01 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"fix(workspace):=20drop=20workspace=20?= =?UTF-8?q?from=20list=20cache=20while=20delete=20is=20pendin=E2=80=A6"=20?= =?UTF-8?q?(#4982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f9b7e5035a58bedf3a702099fc096efa690d6765. --- packages/core/workspace/mutations.test.tsx | 121 --------------------- packages/core/workspace/mutations.ts | 24 ---- 2 files changed, 145 deletions(-) delete mode 100644 packages/core/workspace/mutations.test.tsx diff --git a/packages/core/workspace/mutations.test.tsx b/packages/core/workspace/mutations.test.tsx deleted file mode 100644 index d7e7965e8..000000000 --- a/packages/core/workspace/mutations.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { act, renderHook, waitFor } from "@testing-library/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import type { ReactNode } from "react"; -import { setApiInstance } from "../api"; -import type { ApiClient } from "../api/client"; -import type { Workspace } from "../types"; -import { useDeleteWorkspace } from "./mutations"; -import { workspaceKeys } from "./queries"; - -function createWrapper(qc: QueryClient) { - return function Wrapper({ children }: { children: ReactNode }) { - return {children}; - }; -} - -const makeWorkspace = (id: string, slug: string): Workspace => ({ - id, - name: slug, - slug, - description: null, - context: null, - settings: {}, - repos: [], - issue_prefix: "MUL", - avatar_url: null, - created_at: "2026-01-01T00:00:00Z", - updated_at: "2026-01-01T00:00:00Z", -}); - -describe("useDeleteWorkspace", () => { - let qc: QueryClient; - let deleteWorkspace: ReturnType Promise>>; - - const seedList = () => { - qc.setQueryData(workspaceKeys.list(), [ - makeWorkspace("ws-1", "keep-me"), - makeWorkspace("ws-2", "delete-me"), - ]); - }; - - const cachedList = () => - qc.getQueryData(workspaceKeys.list()) ?? []; - - beforeEach(() => { - qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - deleteWorkspace = vi.fn().mockResolvedValue(undefined); - setApiInstance({ deleteWorkspace } as unknown as ApiClient); - }); - - afterEach(() => { - qc.clear(); - vi.restoreAllMocks(); - }); - - it("removes the workspace from the list cache while the DELETE is pending", async () => { - seedList(); - // Hold the DELETE open so we can observe the pending window — the bug - // was that during this window the list cache still contained the - // workspace, so any consumer re-presented it as selectable/current. - let resolveDelete!: () => void; - deleteWorkspace.mockReturnValue( - new Promise((resolve) => { - resolveDelete = resolve; - }), - ); - - const { result } = renderHook(() => useDeleteWorkspace(), { - wrapper: createWrapper(qc), - }); - - let mutationDone: Promise; - await act(async () => { - mutationDone = result.current.mutateAsync("ws-2"); - // Let onMutate (cancelQueries + optimistic removal) run. - await Promise.resolve(); - }); - - expect(deleteWorkspace).toHaveBeenCalledWith("ws-2"); - expect(cachedList().map((w) => w.id)).toEqual(["ws-1"]); - - await act(async () => { - resolveDelete(); - await mutationDone; - }); - expect(cachedList().map((w) => w.id)).toEqual(["ws-1"]); - }); - - it("invalidates the workspace list after a successful delete", async () => { - seedList(); - const { result } = renderHook(() => useDeleteWorkspace(), { - wrapper: createWrapper(qc), - }); - - await act(async () => { - await result.current.mutateAsync("ws-2"); - }); - - expect(qc.getQueryState(workspaceKeys.list())?.isInvalidated).toBe(true); - }); - - it("rolls the list back when the DELETE fails", async () => { - seedList(); - deleteWorkspace.mockRejectedValue(new Error("boom")); - - const { result } = renderHook(() => useDeleteWorkspace(), { - wrapper: createWrapper(qc), - }); - - await act(async () => { - await expect(result.current.mutateAsync("ws-2")).rejects.toThrow("boom"); - }); - - await waitFor(() => { - expect(cachedList().map((w) => w.id)).toEqual(["ws-1", "ws-2"]); - }); - }); -}); diff --git a/packages/core/workspace/mutations.ts b/packages/core/workspace/mutations.ts index 32e9c7057..f23bb7f96 100644 --- a/packages/core/workspace/mutations.ts +++ b/packages/core/workspace/mutations.ts @@ -39,30 +39,6 @@ export function useDeleteWorkspace() { const qc = useQueryClient(); return useMutation({ mutationFn: (workspaceId: string) => api.deleteWorkspace(workspaceId), - // Optimistically drop the workspace from the list cache while the - // DELETE is in flight. The delete flow navigates away BEFORE awaiting - // the mutation (see workspace-tab.tsx's navigateAwayFromCurrentWorkspace - // for the CancelledError race that forces that ordering), so during the - // pending window every list consumer — sidebar switcher, by-slug route - // resolution, post-auth destination — must already see the workspace as - // gone, or a concurrent list refetch re-presents it as selectable and - // it can be re-entered mid-delete. - onMutate: async (workspaceId) => { - // Cancel in-flight list fetches so a response that started before the - // delete can't land after the optimistic update and resurrect the row. - await qc.cancelQueries({ queryKey: workspaceKeys.list() }); - const previous = qc.getQueryData(workspaceKeys.list()); - qc.setQueryData(workspaceKeys.list(), (old) => - old?.filter((w) => w.id !== workspaceId), - ); - return { previous }; - }, - // Rollback: the server still has the workspace, so put it back in the - // list (the caller surfaces the error toast). onSettled's invalidate - // then reconciles against server truth either way. - onError: (_err, _workspaceId, ctx) => { - if (ctx?.previous) qc.setQueryData(workspaceKeys.list(), ctx.previous); - }, onSettled: () => { qc.invalidateQueries({ queryKey: workspaceKeys.list() }); },