import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { QueryClient } from "@tanstack/react-query"; import type { DaemonClient } from "@server/client/daemon-client"; import { queryClient as appQueryClient } from "@/query/query-client"; import { useSessionStore } from "@/stores/session-store"; import type { WorkspaceDescriptor } from "@/stores/session-store"; import { __resetCheckoutGitActionsStoreForTests, invalidateCheckoutGitQueriesForClient, useCheckoutGitActionsStore, } from "@/stores/checkout-git-actions-store"; vi.mock("@react-native-async-storage/async-storage", () => ({ default: { getItem: vi.fn(async () => null), setItem: vi.fn(async () => undefined), removeItem: vi.fn(async () => undefined), }, })); function createDeferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } function workspace(input: Partial & Pick) { return { id: input.id, projectId: input.projectId ?? "project-1", projectDisplayName: input.projectDisplayName ?? "Project", projectRootPath: input.projectRootPath ?? "/tmp/repo", workspaceDirectory: input.workspaceDirectory ?? input.id, projectKind: input.projectKind ?? "git", workspaceKind: input.workspaceKind ?? "worktree", name: input.name ?? input.id, status: input.status ?? "done", diffStat: input.diffStat ?? null, scripts: input.scripts ?? [], } satisfies WorkspaceDescriptor; } describe("checkout-git-actions-store", () => { const serverId = "server-1"; const cwd = "/tmp/repo"; beforeEach(() => { vi.useFakeTimers(); __resetCheckoutGitActionsStoreForTests(); appQueryClient.clear(); useSessionStore.setState((state) => ({ ...state, sessions: {} as any })); }); afterEach(() => { vi.useRealTimers(); __resetCheckoutGitActionsStoreForTests(); appQueryClient.clear(); useSessionStore.setState((state) => ({ ...state, sessions: {} as any })); }); it("shares pending state per checkout and de-dupes in-flight calls", async () => { const deferred = createDeferred(); const client = { checkoutCommit: vi.fn(() => deferred.promise), }; useSessionStore.setState((state) => ({ ...state, sessions: { ...(state.sessions as any), [serverId]: { client } as any, }, })); const store = useCheckoutGitActionsStore.getState(); const first = store.commit({ serverId, cwd }); const second = store.commit({ serverId, cwd }); expect(client.checkoutCommit).toHaveBeenCalledTimes(1); expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("pending"); deferred.resolve({}); await Promise.all([first, second]); expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("success"); vi.advanceTimersByTime(1000); expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("idle"); }); it("invalidates checkout PR status and every PR pane timeline for a checkout", async () => { const queryClient = new QueryClient(); queryClient.setQueryData(["checkoutPrStatus", serverId, cwd], { status: { number: 12 } }); queryClient.setQueryData(["prPaneTimeline", serverId, cwd, 12], { items: [] }); queryClient.setQueryData(["prPaneTimeline", serverId, cwd, 13], { items: [] }); queryClient.setQueryData(["prPaneTimeline", serverId, "/tmp/other", 12], { items: [] }); await invalidateCheckoutGitQueriesForClient(queryClient, { serverId, cwd }); expect(queryClient.getQueryState(["checkoutPrStatus", serverId, cwd])?.isInvalidated).toBe( true, ); expect(queryClient.getQueryState(["prPaneTimeline", serverId, cwd, 12])?.isInvalidated).toBe( true, ); expect(queryClient.getQueryState(["prPaneTimeline", serverId, cwd, 13])?.isInvalidated).toBe( true, ); expect( queryClient.getQueryState(["prPaneTimeline", serverId, "/tmp/other", 12])?.isInvalidated, ).toBe(false); queryClient.clear(); }); it("hides an archived worktree optimistically while the archive RPC is in flight", async () => { const deferred = createDeferred>(); const client = { archivePaseoWorktree: vi.fn(() => deferred.promise), }; const featureWorkspace = workspace({ id: cwd, name: "feature" }); useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient); useSessionStore.getState().setWorkspaces(serverId, new Map([[cwd, featureWorkspace]])); appQueryClient.setQueryData( ["sidebarPaseoWorktreeList", serverId, "/tmp"], [{ worktreePath: cwd }, { worktreePath: "/tmp/other" }], ); const archive = useCheckoutGitActionsStore .getState() .archiveWorktree({ serverId, cwd, worktreePath: cwd }); expect(client.archivePaseoWorktree).toHaveBeenCalledWith({ worktreePath: cwd }); expect(useSessionStore.getState().sessions[serverId]?.workspaces.has(cwd)).toBe(false); expect(appQueryClient.getQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"])).toEqual([ { worktreePath: "/tmp/other" }, ]); deferred.resolve({}); await archive; }); it("restores an optimistically hidden worktree when archive fails", async () => { const client = { archivePaseoWorktree: vi.fn(async () => ({ error: { message: "archive failed" } })), }; const featureWorkspace = workspace({ id: cwd, name: "feature" }); const listSnapshot = [{ worktreePath: cwd }, { worktreePath: "/tmp/other" }]; useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient); useSessionStore.getState().setWorkspaces(serverId, new Map([[cwd, featureWorkspace]])); appQueryClient.setQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"], listSnapshot); await expect( useCheckoutGitActionsStore.getState().archiveWorktree({ serverId, cwd, worktreePath: cwd }), ).rejects.toThrow("archive failed"); expect(useSessionStore.getState().sessions[serverId]?.workspaces.get(cwd)).toEqual( featureWorkspace, ); expect(appQueryClient.getQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"])).toEqual( listSnapshot, ); }); });