import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { organizationService } from "#/api/organization-service/organization-service.api"; import { useOrganizations } from "#/hooks/query/use-organizations"; import type { Organization } from "#/types/org"; vi.mock("#/api/organization-service/organization-service.api", () => ({ organizationService: { getOrganizations: vi.fn(), }, })); // Mock useIsAuthed to return authenticated vi.mock("#/hooks/query/use-is-authed", () => ({ useIsAuthed: () => ({ data: true }), })); // Mock useConfig to return SaaS mode (organizations are a SaaS-only feature) vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => ({ data: { app_mode: "saas" } }), })); const mockGetOrganizations = vi.mocked(organizationService.getOrganizations); function createMinimalOrg( id: string, name: string, is_personal?: boolean, ): Organization { return { id, name, is_personal, contact_name: "", contact_email: "", conversation_expiration: 0, agent: "", default_max_iterations: 0, security_analyzer: "", confirmation_mode: false, default_llm_model: "", default_llm_api_key_for_byor: "", default_llm_base_url: "", remote_runtime_resource_factor: 0, enable_default_condenser: false, billing_margin: 0, enable_proactive_conversation_starters: false, sandbox_base_container_image: "", sandbox_runtime_container_image: "", org_version: 0, mcp_config: { tools: [], settings: {} }, search_api_key: null, sandbox_api_key: null, max_budget_per_task: 0, enable_solvability_analysis: false, v1_enabled: false, credits: 0, }; } describe("useOrganizations", () => { let queryClient: QueryClient; const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); beforeEach(() => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); vi.clearAllMocks(); }); it("sorts personal workspace first, then non-personal alphabetically by name", async () => { // API returns unsorted: Beta, Personal, Acme, All Hands mockGetOrganizations.mockResolvedValue({ items: [ createMinimalOrg("3", "Beta LLC", false), createMinimalOrg("1", "Personal Workspace", true), createMinimalOrg("2", "Acme Corp", false), createMinimalOrg("4", "All Hands AI", false), ], currentOrgId: "1", }); const { result } = renderHook(() => useOrganizations(), { wrapper }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const { organizations } = result.current.data!; expect(organizations).toHaveLength(4); expect(organizations[0].id).toBe("1"); expect(organizations[0].is_personal).toBe(true); expect(organizations[0].name).toBe("Personal Workspace"); expect(organizations[1].name).toBe("Acme Corp"); expect(organizations[2].name).toBe("All Hands AI"); expect(organizations[3].name).toBe("Beta LLC"); }); it("treats missing is_personal as false and sorts by name", async () => { mockGetOrganizations.mockResolvedValue({ items: [ createMinimalOrg("1", "Zebra Org"), // no is_personal createMinimalOrg("2", "Alpha Org", true), // personal first createMinimalOrg("3", "Mango Org"), // no is_personal ], currentOrgId: "2", }); const { result } = renderHook(() => useOrganizations(), { wrapper }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const { organizations } = result.current.data!; expect(organizations[0].id).toBe("2"); expect(organizations[0].is_personal).toBe(true); expect(organizations[1].name).toBe("Mango Org"); expect(organizations[2].name).toBe("Zebra Org"); }); it("handles missing name by treating as empty string for sort", async () => { const orgWithName = createMinimalOrg("2", "Beta", false); const orgNoName = { ...createMinimalOrg("1", "Alpha", false) }; delete (orgNoName as Record).name; mockGetOrganizations.mockResolvedValue({ items: [orgWithName, orgNoName] as Organization[], currentOrgId: "1", }); const { result } = renderHook(() => useOrganizations(), { wrapper }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const { organizations } = result.current.data!; // undefined name is coerced to ""; "" sorts before "Beta" expect(organizations[0].id).toBe("1"); expect(organizations[1].id).toBe("2"); expect(organizations[1].name).toBe("Beta"); }); it("does not mutate the original array from the API", async () => { const apiOrgs = [ createMinimalOrg("2", "Acme", false), createMinimalOrg("1", "Personal", true), ]; mockGetOrganizations.mockResolvedValue({ items: apiOrgs, currentOrgId: "1", }); const { result } = renderHook(() => useOrganizations(), { wrapper }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Hook sorts a copy ([...data]), so API order unchanged expect(apiOrgs[0].id).toBe("2"); expect(apiOrgs[1].id).toBe("1"); // Returned data is sorted expect(result.current.data!.organizations[0].id).toBe("1"); expect(result.current.data!.organizations[1].id).toBe("2"); }); });