Download __tests__/query-client-config.behavior.test.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/query-client-config.behavior.test.ts
- Command line
-
hf download hf://SaylorTwift/openhands/__tests__/query-client-config.behavior.test.ts
-
curl -L -o query-client-config.behavior.test.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/query-client-config.behavior.test.ts
15.9 kB
| import { QueryClient } from "@tanstack/react-query"; | |
| import { AxiosError } from "axios"; | |
| import { afterEach, describe, expect, it, vi } from "vitest"; | |
| import { createAgentServerQueryClient } from "#/query-client-config"; | |
| import { __resetActiveStoreForTests } from "#/api/backend-registry/active-store"; | |
| import type { Backend } from "#/api/backend-registry/types"; | |
| import { | |
| __resetHealthStoreForTests, | |
| getBackendHealthEntry, | |
| recordBackendFailure, | |
| } from "#/api/backend-registry/health-store"; | |
| import * as ToastHandlers from "#/utils/custom-toast-handlers"; | |
| interface ErrorOptions { | |
| directStatus?: boolean; | |
| message?: string; | |
| status?: number; | |
| url?: string; | |
| } | |
| function createAxiosError({ | |
| directStatus = false, | |
| message = "Request failed", | |
| status, | |
| url, | |
| }: ErrorOptions = {}) { | |
| const error = new AxiosError( | |
| message, | |
| "ERR_BAD_REQUEST", | |
| url ? ({ url } as never) : undefined, | |
| undefined, | |
| !directStatus && status !== undefined ? ({ status } as never) : undefined, | |
| ); | |
| error.status = directStatus ? status : undefined; | |
| return error; | |
| } | |
| function createBackend(overrides: Partial<Backend> = {}): Backend { | |
| return { | |
| id: "local-backend", | |
| name: "Local Backend", | |
| host: "http://localhost:3000", | |
| apiKey: "test-key", | |
| kind: "local", | |
| ...overrides, | |
| }; | |
| } | |
| function activateBackend(backend: Backend) { | |
| const selection = { backendId: backend.id, orgId: null }; | |
| window.localStorage.setItem("openhands-backends", JSON.stringify([backend])); | |
| window.localStorage.setItem( | |
| "openhands-active-backend", | |
| JSON.stringify(selection), | |
| ); | |
| window.sessionStorage.setItem( | |
| "openhands-active-backend", | |
| JSON.stringify(selection), | |
| ); | |
| __resetActiveStoreForTests(); | |
| } | |
| function executeFailingQuery( | |
| client: QueryClient, | |
| error: unknown, | |
| { | |
| meta, | |
| queryKey = ["behavior", "failure"], | |
| }: { | |
| meta?: Record<string, unknown>; | |
| queryKey?: readonly unknown[]; | |
| } = {}, | |
| ) { | |
| return client.fetchQuery({ | |
| queryKey, | |
| queryFn: async () => { | |
| throw error; | |
| }, | |
| meta, | |
| retry: false, | |
| }); | |
| } | |
| function executeFailingMutation( | |
| client: QueryClient, | |
| error: unknown, | |
| meta?: Record<string, unknown>, | |
| ) { | |
| const mutation = client.getMutationCache().build(client, { | |
| mutationFn: async () => { | |
| throw error; | |
| }, | |
| meta, | |
| retry: false, | |
| }); | |
| return mutation.execute(undefined); | |
| } | |
| afterEach(() => { | |
| vi.useRealTimers(); | |
| vi.unstubAllEnvs(); | |
| vi.restoreAllMocks(); | |
| window.localStorage.clear(); | |
| window.sessionStorage.clear(); | |
| delete (window as typeof window & { __OH_QUERY_CLIENT__?: QueryClient }) | |
| .__OH_QUERY_CLIENT__; | |
| __resetActiveStoreForTests(); | |
| __resetHealthStoreForTests(); | |
| }); | |
| describe("query client behavior", () => { | |
| it("records successful queries for string backend identifiers only", async () => { | |
| const client = createAgentServerQueryClient(); | |
| const malformedBackendId = 42; | |
| const malformedMeta = { backendId: malformedBackendId } as unknown as { | |
| backendId: string; | |
| }; | |
| recordBackendFailure("backend-one", new Error("offline")); | |
| recordBackendFailure( | |
| malformedBackendId as unknown as string, | |
| new Error("offline"), | |
| ); | |
| await client.fetchQuery({ | |
| queryKey: ["health", "backend-one"], | |
| queryFn: async () => "healthy", | |
| meta: { backendId: "backend-one" }, | |
| }); | |
| await client.fetchQuery({ | |
| queryKey: ["health", "backend-two"], | |
| queryFn: async () => "healthy", | |
| meta: malformedMeta, | |
| }); | |
| await client.fetchQuery({ | |
| queryKey: ["health", "unattributed"], | |
| queryFn: async () => "healthy", | |
| }); | |
| expect(getBackendHealthEntry("backend-one")).toBeNull(); | |
| expect( | |
| getBackendHealthEntry(malformedBackendId as unknown as string), | |
| ).not.toBeNull(); | |
| }); | |
| it.each([ | |
| { queryKey: ["settings"], description: "an unrelated query" }, | |
| { queryKey: ["user", "profile"], description: "another user query" }, | |
| { | |
| queryKey: ["settings", "authenticated"], | |
| description: "a non-user query ending in authenticated", | |
| }, | |
| ])( | |
| "invalidates authentication after a 401 from $description", | |
| async ({ queryKey }) => { | |
| const client = createAgentServerQueryClient(); | |
| const invalidateQueries = vi | |
| .spyOn(client, "invalidateQueries") | |
| .mockResolvedValue(); | |
| const error = createAxiosError({ status: 401 }); | |
| await expect( | |
| executeFailingQuery(client, error, { | |
| meta: { disableToast: true }, | |
| queryKey, | |
| }), | |
| ).rejects.toBe(error); | |
| expect(invalidateQueries).toHaveBeenCalledWith({ | |
| queryKey: ["user", "authenticated"], | |
| }); | |
| }, | |
| ); | |
| it("does not recursively invalidate authentication when that query fails", async () => { | |
| const client = createAgentServerQueryClient(); | |
| const invalidateQueries = vi | |
| .spyOn(client, "invalidateQueries") | |
| .mockResolvedValue(); | |
| const error = createAxiosError({ status: 401 }); | |
| await expect( | |
| executeFailingQuery(client, error, { | |
| meta: { disableToast: true }, | |
| queryKey: ["user", "authenticated"], | |
| }), | |
| ).rejects.toBe(error); | |
| expect(invalidateQueries).not.toHaveBeenCalled(); | |
| }); | |
| it("recognizes a direct Axios status when a mutation receives a 401", async () => { | |
| const client = createAgentServerQueryClient(); | |
| const invalidateQueries = vi | |
| .spyOn(client, "invalidateQueries") | |
| .mockResolvedValue(); | |
| const error = createAxiosError({ directStatus: true, status: 401 }); | |
| await expect( | |
| executeFailingMutation(client, error, { disableToast: true }), | |
| ).rejects.toBe(error); | |
| expect(invalidateQueries).toHaveBeenCalledWith({ | |
| queryKey: ["user", "authenticated"], | |
| }); | |
| }); | |
| it("does not invalidate authentication for non-401 failures", async () => { | |
| const client = createAgentServerQueryClient(); | |
| const invalidateQueries = vi | |
| .spyOn(client, "invalidateQueries") | |
| .mockResolvedValue(); | |
| const error = createAxiosError({ status: 500 }); | |
| await expect( | |
| executeFailingQuery(client, error, { meta: { disableToast: true } }), | |
| ).rejects.toBe(error); | |
| expect(invalidateQueries).not.toHaveBeenCalled(); | |
| }); | |
| it("preserves null mutation failures without invalidating authentication", async () => { | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const invalidateQueries = vi | |
| .spyOn(client, "invalidateQueries") | |
| .mockResolvedValue(); | |
| await expect(executeFailingMutation(client, null)).rejects.toBeNull(); | |
| expect(invalidateQueries).not.toHaveBeenCalled(); | |
| expect(toast).toHaveBeenCalledWith(expect.any(String)); | |
| }); | |
| it.each([ | |
| { | |
| directStatus: false, | |
| url: undefined, | |
| description: "has no request URL", | |
| }, | |
| { | |
| directStatus: false, | |
| url: "https://cloud.example/api/conversations", | |
| description: "targets the active cloud host", | |
| }, | |
| { | |
| directStatus: true, | |
| url: "https://cloud.example/api/settings", | |
| description: "reports 401 directly for the active cloud host", | |
| }, | |
| ])( | |
| "suppresses a cloud authentication toast when the request $description", | |
| async ({ directStatus, url }) => { | |
| activateBackend( | |
| createBackend({ | |
| id: "cloud-backend", | |
| name: "Cloud Backend", | |
| host: "https://cloud.example///", | |
| kind: "cloud", | |
| }), | |
| ); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const error = createAxiosError({ | |
| directStatus, | |
| message: "Cloud authentication failed", | |
| status: 401, | |
| url, | |
| }); | |
| await expect( | |
| executeFailingQuery(client, error, { | |
| queryKey: ["cloud", url ?? "missing-url"], | |
| }), | |
| ).rejects.toBe(error); | |
| expect(toast).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| it("shows a non-authentication error from the active cloud host", async () => { | |
| activateBackend( | |
| createBackend({ | |
| id: "cloud-backend", | |
| host: "https://cloud.example", | |
| kind: "cloud", | |
| }), | |
| ); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const error = createAxiosError({ | |
| message: "Cloud service unavailable", | |
| status: 503, | |
| url: "https://cloud.example/api/settings", | |
| }); | |
| await expect( | |
| executeFailingQuery(client, error, { | |
| queryKey: ["cloud", "service-unavailable"], | |
| }), | |
| ).rejects.toBe(error); | |
| expect(toast).toHaveBeenCalledWith("Cloud service unavailable"); | |
| }); | |
| it("shows a 401 toast when a cloud request targets another host", async () => { | |
| activateBackend( | |
| createBackend({ | |
| id: "cloud-backend", | |
| host: "https://cloud.example", | |
| kind: "cloud", | |
| }), | |
| ); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const error = createAxiosError({ | |
| message: "Foreign host authentication failed", | |
| status: 401, | |
| url: "https://different.example/api", | |
| }); | |
| await expect( | |
| executeFailingQuery(client, error, { | |
| queryKey: ["cloud", "foreign-host"], | |
| }), | |
| ).rejects.toBe(error); | |
| expect(toast).toHaveBeenCalledWith("Foreign host authentication failed"); | |
| }); | |
| it("shows a 401 toast for an active local backend", async () => { | |
| activateBackend(createBackend()); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const error = createAxiosError({ | |
| message: "Local authentication failed", | |
| status: 401, | |
| }); | |
| await expect( | |
| executeFailingQuery(client, error, { | |
| queryKey: ["local", "authentication"], | |
| }), | |
| ).rejects.toBe(error); | |
| expect(toast).toHaveBeenCalledWith("Local authentication failed"); | |
| }); | |
| it("deduplicates query toasts until the cooldown expires", async () => { | |
| vi.useFakeTimers(); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const first = new AxiosError("Repeated query failure"); | |
| const second = new AxiosError("Repeated query failure"); | |
| await expect( | |
| executeFailingQuery(client, first, { | |
| queryKey: ["dedupe", "first"], | |
| }), | |
| ).rejects.toBe(first); | |
| await expect( | |
| executeFailingQuery(client, second, { | |
| queryKey: ["dedupe", "second"], | |
| }), | |
| ).rejects.toBe(second); | |
| expect(toast).toHaveBeenCalledTimes(1); | |
| await vi.advanceTimersByTimeAsync(3000); | |
| const afterCooldown = new AxiosError("Repeated query failure"); | |
| await expect( | |
| executeFailingQuery(client, afterCooldown, { | |
| queryKey: ["dedupe", "after-cooldown"], | |
| }), | |
| ).rejects.toBe(afterCooldown); | |
| expect(toast).toHaveBeenCalledTimes(2); | |
| }); | |
| it("uses the translated generic query error when no message is available", async () => { | |
| vi.useFakeTimers(); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const first = {}; | |
| const duplicate = {}; | |
| await expect( | |
| executeFailingQuery(client, first, { | |
| queryKey: ["generic", "first"], | |
| }), | |
| ).rejects.toBe(first); | |
| await expect( | |
| executeFailingQuery(client, duplicate, { | |
| queryKey: ["generic", "duplicate"], | |
| }), | |
| ).rejects.toBe(duplicate); | |
| expect(toast).toHaveBeenCalledWith(expect.any(String)); | |
| expect(toast).toHaveBeenCalledTimes(1); | |
| await vi.advanceTimersByTimeAsync(3000); | |
| const afterCooldown = {}; | |
| await expect( | |
| executeFailingQuery(client, afterCooldown, { | |
| queryKey: ["generic", "after-cooldown"], | |
| }), | |
| ).rejects.toBe(afterCooldown); | |
| expect(toast).toHaveBeenCalledTimes(2); | |
| }); | |
| it("honors mutation toast metadata", async () => { | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const visible = new AxiosError("Visible mutation failure"); | |
| const suppressed = new AxiosError("Suppressed mutation failure"); | |
| await expect(executeFailingMutation(client, visible)).rejects.toBe(visible); | |
| await expect( | |
| executeFailingMutation(client, suppressed, { disableToast: true }), | |
| ).rejects.toBe(suppressed); | |
| expect(toast).toHaveBeenCalledOnce(); | |
| expect(toast).toHaveBeenCalledWith("Visible mutation failure"); | |
| }); | |
| it("suppresses matching cloud-auth mutation errors", async () => { | |
| activateBackend( | |
| createBackend({ | |
| id: "cloud-backend", | |
| host: "https://cloud.example", | |
| kind: "cloud", | |
| }), | |
| ); | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const error = createAxiosError({ | |
| message: "Cloud mutation authentication failed", | |
| status: 401, | |
| url: "https://cloud.example/api/settings", | |
| }); | |
| await expect(executeFailingMutation(client, error)).rejects.toBe(error); | |
| expect(toast).not.toHaveBeenCalled(); | |
| }); | |
| it("uses the translated generic mutation error when no message is available", async () => { | |
| const toast = vi.spyOn(ToastHandlers, "displayErrorToast"); | |
| const client = createAgentServerQueryClient(); | |
| const error = {}; | |
| await expect(executeFailingMutation(client, error)).rejects.toBe(error); | |
| expect(toast).toHaveBeenCalledWith(expect.any(String)); | |
| }); | |
| }); | |
| describe("query client selection and proxy behavior", () => { | |
| it("creates one default client and exposes it in development", async () => { | |
| vi.resetModules(); | |
| const config = await import("#/query-client-config"); | |
| const first = config.getDefaultQueryClient(); | |
| const second = config.getDefaultQueryClient(); | |
| expect(first).toBeInstanceOf(QueryClient); | |
| expect(second).toBe(first); | |
| expect(config.getQueryClient()).toBe(first); | |
| expect( | |
| (window as typeof window & { __OH_QUERY_CLIENT__?: QueryClient }) | |
| .__OH_QUERY_CLIENT__, | |
| ).toBe(first); | |
| }); | |
| it("selects custom clients and forwards proxy reads, calls, and writes", async () => { | |
| vi.resetModules(); | |
| const config = await import("#/query-client-config"); | |
| const custom = new QueryClient(); | |
| expect(config.setQueryClient(custom)).toBe(custom); | |
| expect(config.getQueryClient()).toBe(custom); | |
| config.queryClient.setQueryData(["proxy", "value"], "forwarded"); | |
| expect(custom.getQueryData(["proxy", "value"])).toBe("forwarded"); | |
| const extendedProxy = config.queryClient as QueryClient & { | |
| marker?: string; | |
| }; | |
| const extendedClient = custom as QueryClient & { marker?: string }; | |
| extendedProxy.marker = "proxy-write"; | |
| expect(extendedProxy.marker).toBe("proxy-write"); | |
| expect(extendedClient.marker).toBe("proxy-write"); | |
| expect(config.setQueryClient(undefined)).toBe( | |
| config.getDefaultQueryClient(), | |
| ); | |
| expect(config.setQueryClient(null)).toBe(config.getDefaultQueryClient()); | |
| }); | |
| it.each([ | |
| { mockApi: "true", expectedExposure: true }, | |
| { mockApi: "false", expectedExposure: false }, | |
| ])( | |
| "sets window exposure to $expectedExposure outside development when VITE_MOCK_API is $mockApi", | |
| async ({ expectedExposure, mockApi }) => { | |
| vi.stubEnv("DEV", false); | |
| vi.stubEnv("VITE_MOCK_API", mockApi); | |
| vi.resetModules(); | |
| const config = await import("#/query-client-config"); | |
| const client = config.getDefaultQueryClient(); | |
| const exposed = ( | |
| window as typeof window & { __OH_QUERY_CLIENT__?: QueryClient } | |
| ).__OH_QUERY_CLIENT__; | |
| if (expectedExposure) { | |
| expect(exposed).toBe(client); | |
| } else { | |
| expect(exposed).toBeUndefined(); | |
| } | |
| }, | |
| ); | |
| }); | |