diff --git a/src/hooks/useSkills.ts b/src/hooks/useSkills.ts index 054b8778c..dd03a8b0d 100644 --- a/src/hooks/useSkills.ts +++ b/src/hooks/useSkills.ts @@ -288,15 +288,18 @@ export function useImportSkillsFromApps() { }, // Import may persist Skills or auto-discovered repositories before a // later item fails, so refresh every affected authoritative collection. - onSettled: () => - Promise.all([ + // 只等导入弹窗关心的两个列表:mutation 要等 onSettled 返回的 Promise + // 才结束,「发现」重拉要从 GitHub 下载仓库,等它会让弹窗一直锁着(#7994) + onSettled: () => { + void queryClient.invalidateQueries({ queryKey: ["skills", "repos"] }); + void queryClient.invalidateQueries({ + queryKey: ["skills", "discoverable"], + }); + return Promise.all([ queryClient.invalidateQueries({ queryKey: ["skills", "installed"] }), queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] }), - queryClient.invalidateQueries({ queryKey: ["skills", "repos"] }), - queryClient.invalidateQueries({ - queryKey: ["skills", "discoverable"], - }), - ]), + ]); + }, }); } diff --git a/tests/hooks/useImportSkillsFromApps.test.tsx b/tests/hooks/useImportSkillsFromApps.test.tsx index 155378c88..97a6ff915 100644 --- a/tests/hooks/useImportSkillsFromApps.test.tsx +++ b/tests/hooks/useImportSkillsFromApps.test.tsx @@ -285,16 +285,52 @@ describe("Skills install and import mutation hooks", () => { }); }); - it("keeps a rejected import pending until all affected caches refresh", async () => { - let releaseInvalidation: (() => void) | undefined; - const invalidationPending = new Promise((resolve) => { - releaseInvalidation = resolve; + // #7994:「发现」重拉要从 GitHub 下载仓库,可能很久都不返回;导入只等 + // installed / unmanaged,不能因为它一直 pending、把导入弹窗锁住 + function spyImportInvalidations(queryClient: QueryClient) { + let releaseLocal: (() => void) | undefined; + const localPending = new Promise((resolve) => { + releaseLocal = resolve; }); + const spy = vi + .spyOn(queryClient, "invalidateQueries") + .mockImplementation((filters) => { + const scope = filters?.queryKey?.[1]; + return scope === "installed" || scope === "unmanaged" + ? localPending + : new Promise(() => undefined); + }); + return { spy, releaseLocal: () => releaseLocal?.() }; + } + + it("finishes an import without waiting for repos or discover to refetch", async () => { + const queryClient = createQueryClient(); + const imported = makeSkill({ name: "Imported Skill" }); + apiMocks.importFromApps.mockResolvedValueOnce([imported]); + const { spy, releaseLocal } = spyImportInvalidations(queryClient); + const { result } = renderHook(() => useImportSkillsFromApps(), { + wrapper: createWrapper(queryClient), + }); + + let mutation!: Promise; + act(() => { + mutation = result.current.mutateAsync([]); + }); + + await waitFor(() => expect(spy).toHaveBeenCalledTimes(4)); + expect(result.current.isPending).toBe(true); + + releaseLocal(); + await act(async () => { + await expect(mutation).resolves.toEqual([imported]); + }); + await waitFor(() => expect(result.current.isPending).toBe(false)); + }); + + it("keeps a rejected import pending only until installed and unmanaged refresh", async () => { const queryClient = createQueryClient(); apiMocks.importFromApps.mockRejectedValueOnce(new Error("import failed")); - const invalidateSpy = vi - .spyOn(queryClient, "invalidateQueries") - .mockImplementation(() => invalidationPending); + const { spy, releaseLocal } = spyImportInvalidations(queryClient); const { result } = renderHook(() => useImportSkillsFromApps(), { wrapper: createWrapper(queryClient), }); @@ -305,7 +341,7 @@ describe("Skills install and import mutation hooks", () => { void mutation.catch(() => undefined); }); - await waitFor(() => expect(invalidateSpy).toHaveBeenCalledTimes(4)); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(4)); expect(result.current.isPending).toBe(true); for (const queryKey of [ ["skills", "installed"], @@ -313,10 +349,10 @@ describe("Skills install and import mutation hooks", () => { ["skills", "repos"], ["skills", "discoverable"], ]) { - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey }); + expect(spy).toHaveBeenCalledWith({ queryKey }); } - releaseInvalidation?.(); + releaseLocal(); await act(async () => { await expect(mutation).rejects.toThrow("import failed"); });