Download __tests__/hooks/query/git-changes-ordering.test.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/hooks/query/git-changes-ordering.test.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/__tests__/hooks/query/git-changes-ordering.test.tsx
-
curl -L -o git-changes-ordering.test.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/hooks/query/git-changes-ordering.test.tsx
13.4 kB
| import React from "react"; | |
| import { act, renderHook, waitFor } from "@testing-library/react"; | |
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | |
| import { describe, expect, it, vi } from "vitest"; | |
| import AgentServerGitService from "#/api/git-service/agent-server-git-service.api"; | |
| import type { GitChange } from "#/api/open-hands.types"; | |
| import type { AppConversation } from "#/api/conversation-service/agent-server-conversation-service.types"; | |
| import { useUnifiedGetGitChanges } from "#/hooks/query/use-unified-get-git-changes"; | |
| const mocks = vi.hoisted(() => ({ | |
| conversationId: "conversation-1" as string | undefined, | |
| conversation: undefined as AppConversation | undefined, | |
| runtimeIsReady: true, | |
| })); | |
| vi.mock("#/hooks/use-conversation-id", () => ({ | |
| useConversationId: () => ({ conversationId: mocks.conversationId }), | |
| })); | |
| vi.mock("#/hooks/query/use-active-conversation", () => ({ | |
| useActiveConversation: () => ({ data: mocks.conversation }), | |
| })); | |
| vi.mock("#/hooks/use-runtime-is-ready", () => ({ | |
| useRuntimeIsReady: () => mocks.runtimeIsReady, | |
| })); | |
| const getGitChanges = vi.spyOn(AgentServerGitService, "getGitChanges"); | |
| const makeConversation = ( | |
| overrides: Partial<AppConversation> = {}, | |
| ): AppConversation => ({ | |
| id: "conversation-1", | |
| created_by_user_id: "user-1", | |
| selected_repository: "OpenHands/agent-canvas", | |
| selected_branch: "main", | |
| git_provider: "github", | |
| title: "Mutation-tested changes", | |
| trigger: "gui", | |
| pr_number: [], | |
| llm_model: null, | |
| metrics: null, | |
| created_at: "2026-07-13T00:00:00.000Z", | |
| updated_at: "2026-07-13T00:00:00.000Z", | |
| execution_status: null, | |
| conversation_url: "https://runtime.example.test/conversations/conversation-1", | |
| session_api_key: "session-key", | |
| sandbox_id: "sandbox-1", | |
| workspace: { working_dir: "/workspace/agent-canvas" }, | |
| sub_conversation_ids: [], | |
| ...overrides, | |
| }); | |
| const makeChange = ( | |
| path: string, | |
| status: GitChange["status"] = "M", | |
| ): GitChange => ({ path, status }); | |
| function makeDeferred<T>() { | |
| const resolver = { current: (_value: T) => {} }; | |
| const promise = new Promise<T>((resolve) => { | |
| resolver.current = resolve; | |
| }); | |
| return { | |
| promise, | |
| resolve: (value: T) => resolver.current(value), | |
| }; | |
| } | |
| function prepareHook( | |
| overrides: { | |
| conversationId?: string | undefined; | |
| conversation?: AppConversation | undefined; | |
| runtimeIsReady?: boolean; | |
| } = {}, | |
| ) { | |
| getGitChanges.mockReset(); | |
| mocks.conversationId = Object.hasOwn(overrides, "conversationId") | |
| ? overrides.conversationId | |
| : "conversation-1"; | |
| mocks.conversation = Object.hasOwn(overrides, "conversation") | |
| ? overrides.conversation | |
| : makeConversation(); | |
| mocks.runtimeIsReady = overrides.runtimeIsReady ?? true; | |
| const queryClient = new QueryClient({ | |
| defaultOptions: { | |
| queries: { | |
| retry: false, | |
| gcTime: Number.POSITIVE_INFINITY, | |
| }, | |
| }, | |
| }); | |
| const wrapper = ({ children }: { children: React.ReactNode }) => ( | |
| <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> | |
| ); | |
| return { queryClient, wrapper }; | |
| } | |
| describe("git change loading and ordering", () => { | |
| it("loads the current workspace changes with runtime credentials", async () => { | |
| const changes = [ | |
| makeChange("src/new-file.ts", "A"), | |
| makeChange("src/existing.ts"), | |
| ]; | |
| const { wrapper } = prepareHook(); | |
| getGitChanges.mockResolvedValue(changes); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.isSuccess).toBe(true)); | |
| expect(result.current).toMatchObject({ | |
| data: changes, | |
| isLoading: false, | |
| isFetching: false, | |
| isSuccess: true, | |
| isError: false, | |
| error: null, | |
| }); | |
| expect(result.current.refetch).toEqual(expect.any(Function)); | |
| expect(getGitChanges).toHaveBeenCalledOnce(); | |
| expect(getGitChanges).toHaveBeenCalledWith( | |
| "conversation-1", | |
| "https://runtime.example.test/conversations/conversation-1", | |
| "session-key", | |
| "/workspace/agent-canvas", | |
| ); | |
| }); | |
| it("derives the git path from the selected repository when the workspace is blank", async () => { | |
| const { wrapper } = prepareHook({ | |
| conversation: makeConversation({ | |
| selected_repository: "OpenHands/software-agent-sdk", | |
| workspace: { working_dir: " " }, | |
| }), | |
| }); | |
| getGitChanges.mockResolvedValue([]); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.isSuccess).toBe(true)); | |
| expect(getGitChanges).toHaveBeenCalledWith( | |
| "conversation-1", | |
| expect.any(String), | |
| "session-key", | |
| "workspace/project/software-agent-sdk", | |
| ); | |
| expect(result.current.data).toEqual([]); | |
| }); | |
| it("uses the default git path while conversation metadata is still unavailable", async () => { | |
| const { wrapper } = prepareHook({ conversation: undefined }); | |
| getGitChanges.mockResolvedValue([makeChange("README.md")]); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.isSuccess).toBe(true)); | |
| expect(getGitChanges).toHaveBeenCalledWith( | |
| "conversation-1", | |
| undefined, | |
| undefined, | |
| "workspace/project", | |
| ); | |
| }); | |
| it("uses the selected repository when its workspace has no working directory", async () => { | |
| const { wrapper } = prepareHook({ | |
| conversation: makeConversation({ | |
| selected_repository: "OpenHands/openhands", | |
| workspace: { working_dir: null }, | |
| }), | |
| }); | |
| getGitChanges.mockResolvedValue([]); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.isSuccess).toBe(true)); | |
| expect(getGitChanges).toHaveBeenCalledWith( | |
| "conversation-1", | |
| expect.any(String), | |
| "session-key", | |
| "workspace/project/openhands", | |
| ); | |
| }); | |
| it("does not contact the backend until the runtime is ready", async () => { | |
| const { wrapper } = prepareHook({ runtimeIsReady: false }); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await act(async () => { | |
| await Promise.resolve(); | |
| }); | |
| expect(getGitChanges).not.toHaveBeenCalled(); | |
| expect(result.current).toMatchObject({ | |
| data: [], | |
| isLoading: false, | |
| isFetching: false, | |
| isSuccess: false, | |
| isError: false, | |
| }); | |
| }); | |
| it("reports a clear error if a disabled query is manually run without a conversation id", async () => { | |
| const { wrapper } = prepareHook({ conversationId: undefined }); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await act(async () => { | |
| await result.current.refetch(); | |
| }); | |
| await waitFor(() => expect(result.current.isError).toBe(true)); | |
| expect(result.current.error).toEqual(new Error("No conversation ID")); | |
| expect(getGitChanges).not.toHaveBeenCalled(); | |
| }); | |
| it("puts newly discovered files first and removes files absent from a refresh", async () => { | |
| const originalChanges = [ | |
| makeChange("src/removed.ts", "D"), | |
| makeChange("src/kept.ts"), | |
| ]; | |
| const refreshedChanges = [ | |
| makeChange("src/kept.ts"), | |
| makeChange("src/new.ts", "A"), | |
| ]; | |
| const { wrapper } = prepareHook(); | |
| getGitChanges | |
| .mockResolvedValueOnce(originalChanges) | |
| .mockResolvedValueOnce(refreshedChanges); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.data).toEqual(originalChanges)); | |
| await act(async () => { | |
| await result.current.refetch(); | |
| }); | |
| await waitFor(() => | |
| expect(result.current.data).toEqual([ | |
| makeChange("src/new.ts", "A"), | |
| makeChange("src/kept.ts"), | |
| ]), | |
| ); | |
| expect(getGitChanges).toHaveBeenCalledTimes(2); | |
| }); | |
| it("ignores a refreshed payload when the backend returns the same array reference", async () => { | |
| const sharedChanges = [makeChange("src/original.ts")]; | |
| const refresh = makeDeferred<GitChange[]>(); | |
| const { wrapper } = prepareHook(); | |
| getGitChanges | |
| .mockResolvedValueOnce(sharedChanges) | |
| .mockReturnValueOnce(refresh.promise); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.data).toEqual(sharedChanges)); | |
| sharedChanges.push(makeChange("src/mutated-in-place.ts", "A")); | |
| act(() => { | |
| void result.current.refetch(); | |
| }); | |
| await waitFor(() => expect(result.current.isFetching).toBe(true)); | |
| await act(async () => { | |
| refresh.resolve(sharedChanges); | |
| }); | |
| await waitFor(() => expect(result.current.isFetching).toBe(false)); | |
| expect(getGitChanges).toHaveBeenCalledTimes(2); | |
| expect(result.current.data).toEqual([makeChange("src/original.ts")]); | |
| }); | |
| it("normalizes a single change returned by a legacy backend", async () => { | |
| const singleChange = makeChange("legacy.txt", "U"); | |
| const { wrapper } = prepareHook(); | |
| getGitChanges.mockResolvedValue( | |
| singleChange as unknown as Awaited< | |
| ReturnType<typeof AgentServerGitService.getGitChanges> | |
| >, | |
| ); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.isSuccess).toBe(true)); | |
| expect(result.current.data).toEqual([singleChange]); | |
| }); | |
| it("surfaces backend failures without retrying", async () => { | |
| const backendError = new Error("runtime unavailable"); | |
| const { queryClient, wrapper } = prepareHook(); | |
| getGitChanges.mockRejectedValue(backendError); | |
| const { result } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(result.current.isError).toBe(true)); | |
| expect(result.current.error).toBe(backendError); | |
| expect(result.current.data).toEqual([]); | |
| expect(getGitChanges).toHaveBeenCalledOnce(); | |
| const query = queryClient.getQueryCache().find({ | |
| queryKey: [ | |
| "file_changes", | |
| "conversation-1", | |
| "https://runtime.example.test/conversations/conversation-1", | |
| "session-key", | |
| "/workspace/agent-canvas", | |
| ], | |
| }); | |
| expect(query?.options).toMatchObject({ | |
| retry: false, | |
| staleTime: 300_000, | |
| gcTime: 900_000, | |
| refetchOnMount: "always", | |
| meta: { disableToast: true }, | |
| }); | |
| }); | |
| it("keeps query caches isolated across every runtime identity field", async () => { | |
| const { wrapper } = prepareHook(); | |
| getGitChanges.mockImplementation(async () => [ | |
| makeChange(`response-${getGitChanges.mock.calls.length}.txt`), | |
| ]); | |
| const { rerender } = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(1)); | |
| mocks.conversationId = "conversation-2"; | |
| rerender(); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(2)); | |
| mocks.conversation = makeConversation({ | |
| conversation_url: "https://runtime-2.example.test/conversations/2", | |
| }); | |
| rerender(); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(3)); | |
| mocks.conversation = makeConversation({ session_api_key: "session-key-2" }); | |
| rerender(); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(4)); | |
| mocks.conversation = makeConversation({ | |
| workspace: { working_dir: "/workspace/other" }, | |
| }); | |
| rerender(); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(5)); | |
| mocks.conversation = makeConversation({ | |
| selected_repository: "OpenHands/another-repository", | |
| workspace: null, | |
| }); | |
| rerender(); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(6)); | |
| }); | |
| it("refetches on remount even while the cached changes are fresh", async () => { | |
| const { queryClient, wrapper } = prepareHook(); | |
| const refresh = makeDeferred<GitChange[]>(); | |
| getGitChanges | |
| .mockResolvedValueOnce([makeChange("cached.ts")]) | |
| .mockReturnValueOnce(refresh.promise); | |
| const firstRender = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledOnce()); | |
| await waitFor(() => | |
| expect(firstRender.result.current.data).toEqual([ | |
| makeChange("cached.ts"), | |
| ]), | |
| ); | |
| firstRender.unmount(); | |
| const secondRender = renderHook(() => useUnifiedGetGitChanges(), { | |
| wrapper, | |
| }); | |
| await waitFor(() => expect(getGitChanges).toHaveBeenCalledTimes(2)); | |
| await waitFor(() => | |
| expect(secondRender.result.current.isFetching).toBe(true), | |
| ); | |
| expect(secondRender.result.current.data).toEqual([]); | |
| await act(async () => { | |
| refresh.resolve([makeChange("refreshed.ts")]); | |
| }); | |
| await waitFor(() => | |
| expect(secondRender.result.current.data).toEqual([ | |
| makeChange("refreshed.ts"), | |
| ]), | |
| ); | |
| expect( | |
| queryClient.getQueryState([ | |
| "file_changes", | |
| "conversation-1", | |
| "https://runtime.example.test/conversations/conversation-1", | |
| "session-key", | |
| "/workspace/agent-canvas", | |
| ])?.status, | |
| ).toBe("success"); | |
| }); | |
| }); | |