import { renderHook, act } from "@testing-library/react"; import { describe, expect, it, vi, beforeEach } from "vitest"; const mockClaimMutate = vi.fn(); const mockDisconnectMutate = vi.fn(); vi.mock("#/hooks/query/use-git-organizations", () => ({ useUserGitOrganizations: vi.fn(), useGitClaims: vi.fn(), })); vi.mock("#/hooks/mutation/use-claim-git-org", () => ({ useClaimGitOrg: () => ({ mutate: mockClaimMutate }), })); vi.mock("#/hooks/mutation/use-disconnect-git-org", () => ({ useDisconnectGitOrg: () => ({ mutate: mockDisconnectMutate }), })); import { useGitConversationRouting } from "#/hooks/organizations/use-git-conversation-routing"; import { useUserGitOrganizations, useGitClaims, } from "#/hooks/query/use-git-organizations"; const mockUseUserGitOrganizations = vi.mocked(useUserGitOrganizations); const mockUseGitClaims = vi.mocked(useGitClaims); function setupMocks({ userOrgs = { provider: "github", organizations: ["OpenHands", "AcmeCo"] }, claims = [] as Array<{ id: string; org_id: string; provider: string; git_organization: string; claimed_by: string; claimed_at: string; }>, isLoadingUserOrgs = false, isLoadingClaims = false, } = {}) { mockUseUserGitOrganizations.mockReturnValue({ data: userOrgs, isLoading: isLoadingUserOrgs, } as ReturnType); mockUseGitClaims.mockReturnValue({ data: claims, isLoading: isLoadingClaims, } as ReturnType); } describe("useGitConversationRouting", () => { beforeEach(() => { vi.clearAllMocks(); }); it("returns empty orgs when user git orgs is undefined", () => { mockUseUserGitOrganizations.mockReturnValue({ data: undefined, isLoading: true, } as ReturnType); mockUseGitClaims.mockReturnValue({ data: undefined, isLoading: true, } as ReturnType); const { result } = renderHook(() => useGitConversationRouting()); expect(result.current.orgs).toEqual([]); expect(result.current.isLoading).toBe(true); }); it("merges user git orgs with claims correctly", () => { setupMocks({ claims: [ { id: "claim-1", org_id: "org-1", provider: "github", git_organization: "wren", claimed_by: "user-1", claimed_at: "2026-01-01T00:00:00", }, ], }); const { result } = renderHook(() => useGitConversationRouting()); expect(result.current.orgs).toHaveLength(2); const claimedOrg = result.current.orgs.find((o) => o.name === "OpenHands"); expect(claimedOrg).toMatchObject({ id: "github:wren", claimId: "claim-1", provider: "github", status: "claimed", }); const unclaimedOrg = result.current.orgs.find((o) => o.name === "AcmeCo"); expect(unclaimedOrg).toMatchObject({ id: "github:acmeco", claimId: null, provider: "github", status: "unclaimed", }); }); it("handles case-insensitive matching between user orgs and claims", () => { setupMocks({ userOrgs: { provider: "GitHub", organizations: ["All-Hands-AI"], }, claims: [ { id: "claim-1", org_id: "org-1", provider: "github", git_organization: "wren-ai", claimed_by: "user-1", claimed_at: "2026-01-01T00:00:00", }, ], }); const { result } = renderHook(() => useGitConversationRouting()); expect(result.current.orgs[0]).toMatchObject({ status: "claimed", claimId: "claim-1", name: "All-Hands-AI", }); }); it("supports Bitbucket Data Center projects", () => { setupMocks({ userOrgs: { provider: "bitbucket_data_center", organizations: ["PROJ"], }, claims: [ { id: "claim-1", org_id: "org-1", provider: "bitbucket_data_center", git_organization: "proj", claimed_by: "user-1", claimed_at: "2026-01-01T00:00:00", }, ], }); const { result } = renderHook(() => useGitConversationRouting()); expect(result.current.orgs[0]).toMatchObject({ id: "bitbucket_data_center:proj", claimId: "claim-1", provider: "bitbucket_data_center", name: "PROJ", status: "claimed", }); }); it("returns unclaimed when no claims data is available", () => { setupMocks({ claims: [] }); const { result } = renderHook(() => useGitConversationRouting()); expect(result.current.orgs).toHaveLength(2); expect(result.current.orgs.every((o) => o.status === "unclaimed")).toBe( true, ); expect(result.current.orgs.every((o) => o.claimId === null)).toBe(true); }); it("sets pending claiming state when claimOrg is called", () => { setupMocks(); const { result } = renderHook(() => useGitConversationRouting()); act(() => { result.current.claimOrg("github:acmeco"); }); expect(mockClaimMutate).toHaveBeenCalledWith( { provider: "github", gitOrganization: "AcmeCo" }, expect.objectContaining({ onSettled: expect.any(Function) }), ); const org = result.current.orgs.find((o) => o.id === "github:acmeco"); expect(org?.status).toBe("claiming"); }); it("sets pending disconnecting state when disconnectOrg is called", () => { setupMocks({ claims: [ { id: "claim-1", org_id: "org-1", provider: "github", git_organization: "wren", claimed_by: "user-1", claimed_at: "2026-01-01T00:00:00", }, ], }); const { result } = renderHook(() => useGitConversationRouting()); act(() => { result.current.disconnectOrg("github:wren"); }); expect(mockDisconnectMutate).toHaveBeenCalledWith( { claimId: "claim-1" }, expect.objectContaining({ onSettled: expect.any(Function) }), ); const org = result.current.orgs.find((o) => o.id === "github:wren"); expect(org?.status).toBe("disconnecting"); }); it("clears pending state on settle", () => { setupMocks(); mockClaimMutate.mockImplementation( (_args: unknown, options: { onSettled?: () => void }) => { options?.onSettled?.(); }, ); const { result } = renderHook(() => useGitConversationRouting()); act(() => { result.current.claimOrg("github:acmeco"); }); const org = result.current.orgs.find((o) => o.id === "github:acmeco"); expect(org?.status).toBe("unclaimed"); }); it("does not claim an already claimed org", () => { setupMocks({ claims: [ { id: "claim-1", org_id: "org-1", provider: "github", git_organization: "wren", claimed_by: "user-1", claimed_at: "2026-01-01T00:00:00", }, ], }); const { result } = renderHook(() => useGitConversationRouting()); act(() => { result.current.claimOrg("github:wren"); }); expect(mockClaimMutate).not.toHaveBeenCalled(); }); it("does not disconnect an unclaimed org", () => { setupMocks(); const { result } = renderHook(() => useGitConversationRouting()); act(() => { result.current.disconnectOrg("github:acmeco"); }); expect(mockDisconnectMutate).not.toHaveBeenCalled(); }); });