Revert "fix(workspace): drop workspace from list cache while delete is pendin…" (#4982)

This reverts commit f9b7e5035a.
This commit is contained in:
Naiyuan Qing
2026-07-06 17:02:01 +08:00
committed by GitHub
parent f9b7e5035a
commit 152edab8d0
2 changed files with 0 additions and 145 deletions

View File

@@ -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 <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
};
}
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<typeof vi.fn<(id: string) => Promise<void>>>;
const seedList = () => {
qc.setQueryData<Workspace[]>(workspaceKeys.list(), [
makeWorkspace("ws-1", "keep-me"),
makeWorkspace("ws-2", "delete-me"),
]);
};
const cachedList = () =>
qc.getQueryData<Workspace[]>(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<void>((resolve) => {
resolveDelete = resolve;
}),
);
const { result } = renderHook(() => useDeleteWorkspace(), {
wrapper: createWrapper(qc),
});
let mutationDone: Promise<void>;
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"]);
});
});
});

View File

@@ -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<Workspace[]>(workspaceKeys.list());
qc.setQueryData<Workspace[]>(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() });
},