From 7281c73834f5820d595330969e989b4089b52086 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 1 Apr 2026 16:49:29 -0700 Subject: [PATCH] Scope git query invalidation by cwd - Invalidate only status/branches for the active cwd - Add tests for cwd-scoped invalidation - Co-authored-by: codex --- apps/web/src/components/GitActionsControl.tsx | 7 +- apps/web/src/lib/gitReactQuery.test.ts | 64 ++++++++++++++++++- apps/web/src/lib/gitReactQuery.ts | 18 +++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 6384709620..bfe1e7e569 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -45,6 +45,7 @@ import { gitPullMutationOptions, gitRunStackedActionMutationOptions, gitStatusQueryOptions, + invalidateGitStatusQuery, invalidateGitQueries, } from "~/lib/gitReactQuery"; import { randomUUID } from "~/lib/utils"; @@ -243,8 +244,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions useEffect(() => { if (!isGitStatusOutOfSync) return; - void invalidateGitQueries(queryClient); - }, [isGitStatusOutOfSync, queryClient]); + void invalidateGitQueries(queryClient, { cwd: gitCwd }); + }, [gitCwd, isGitStatusOutOfSync, queryClient]); const gitStatusForActions = isGitStatusOutOfSync ? null : gitStatus; @@ -778,7 +779,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions { - if (open) void invalidateGitQueries(queryClient); + if (open) void invalidateGitStatusQuery(queryClient, gitCwd); }} > ({ + ensureNativeApi: vi.fn(), +})); + +vi.mock("../wsRpcClient", () => ({ + getWsRpcClient: vi.fn(), +})); + import { + gitBranchesQueryOptions, gitMutationKeys, + gitQueryKeys, gitPreparePullRequestThreadMutationOptions, gitPullMutationOptions, gitRunStackedActionMutationOptions, + invalidateGitStatusQuery, + gitStatusQueryOptions, + invalidateGitQueries, } from "./gitReactQuery"; describe("gitMutationKeys", () => { @@ -49,3 +63,51 @@ describe("git mutation options", () => { expect(options.mutationKey).toEqual(gitMutationKeys.preparePullRequestThread("/repo/a")); }); }); + +describe("invalidateGitQueries", () => { + it("can invalidate a single cwd without blasting other git query scopes", async () => { + const queryClient = new QueryClient(); + + queryClient.setQueryData(gitQueryKeys.status("/repo/a"), { ok: "a" }); + queryClient.setQueryData(gitQueryKeys.branches("/repo/a"), { ok: "a-branches" }); + queryClient.setQueryData(gitQueryKeys.status("/repo/b"), { ok: "b" }); + queryClient.setQueryData(gitQueryKeys.branches("/repo/b"), { ok: "b-branches" }); + + await invalidateGitQueries(queryClient, { cwd: "/repo/a" }); + + expect( + queryClient.getQueryState(gitStatusQueryOptions("/repo/a").queryKey)?.isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(gitBranchesQueryOptions("/repo/a").queryKey)?.isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(gitStatusQueryOptions("/repo/b").queryKey)?.isInvalidated, + ).toBe(false); + expect( + queryClient.getQueryState(gitBranchesQueryOptions("/repo/b").queryKey)?.isInvalidated, + ).toBe(false); + }); +}); + +describe("invalidateGitStatusQuery", () => { + it("invalidates only status for the selected cwd", async () => { + const queryClient = new QueryClient(); + + queryClient.setQueryData(gitQueryKeys.status("/repo/a"), { ok: "a" }); + queryClient.setQueryData(gitQueryKeys.branches("/repo/a"), { ok: "a-branches" }); + queryClient.setQueryData(gitQueryKeys.status("/repo/b"), { ok: "b" }); + + await invalidateGitStatusQuery(queryClient, "/repo/a"); + + expect( + queryClient.getQueryState(gitStatusQueryOptions("/repo/a").queryKey)?.isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(gitBranchesQueryOptions("/repo/a").queryKey)?.isInvalidated, + ).toBe(false); + expect( + queryClient.getQueryState(gitStatusQueryOptions("/repo/b").queryKey)?.isInvalidated, + ).toBe(false); + }); +}); diff --git a/apps/web/src/lib/gitReactQuery.ts b/apps/web/src/lib/gitReactQuery.ts index d68e400257..25411db7bd 100644 --- a/apps/web/src/lib/gitReactQuery.ts +++ b/apps/web/src/lib/gitReactQuery.ts @@ -23,10 +23,26 @@ export const gitMutationKeys = { ["git", "mutation", "prepare-pull-request-thread", cwd] as const, }; -export function invalidateGitQueries(queryClient: QueryClient) { +export function invalidateGitQueries(queryClient: QueryClient, input?: { cwd?: string | null }) { + const cwd = input?.cwd ?? null; + if (cwd !== null) { + return Promise.all([ + queryClient.invalidateQueries({ queryKey: gitQueryKeys.status(cwd) }), + queryClient.invalidateQueries({ queryKey: gitQueryKeys.branches(cwd) }), + ]); + } + return queryClient.invalidateQueries({ queryKey: gitQueryKeys.all }); } +export function invalidateGitStatusQuery(queryClient: QueryClient, cwd: string | null) { + if (cwd === null) { + return Promise.resolve(); + } + + return queryClient.invalidateQueries({ queryKey: gitQueryKeys.status(cwd) }); +} + export function gitStatusQueryOptions(cwd: string | null) { return queryOptions({ queryKey: gitQueryKeys.status(cwd),