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() });
},