Skip to content

Commit e1d8860

Browse files
committed
test(zoo-gateway): cover useZooGatewayRouterModelsSync hook
Adds unit tests for the credential-driven model sync: skips when signed out, runs on zooGatewayCredentialsReady and on the sign-in transition, merges without clobbering other providers, and swallows fetch errors. Addresses CodeRabbit review feedback on PR #490.
1 parent 06634f0 commit e1d8860

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// npx vitest src/components/ui/hooks/__tests__/useZooGatewayRouterModelsSync.spec.ts
2+
3+
import React from "react"
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5+
import { renderHook, waitFor } from "@testing-library/react"
6+
import type { Mock } from "vitest"
7+
8+
import type { ModelInfo, RouterModels } from "@roo-code/types"
9+
10+
import { useZooGatewayRouterModelsSync } from "../useZooGatewayRouterModelsSync"
11+
import { fetchRouterModels } from "../useRouterModels"
12+
import { useExtensionState } from "@src/context/ExtensionStateContext"
13+
14+
vi.mock("../useRouterModels")
15+
vi.mock("@src/context/ExtensionStateContext")
16+
17+
const mockFetchRouterModels = fetchRouterModels as Mock<typeof fetchRouterModels>
18+
const mockUseExtensionState = useExtensionState as unknown as Mock
19+
20+
const modelInfo: ModelInfo = {
21+
maxTokens: 8192,
22+
contextWindow: 200000,
23+
supportsImages: false,
24+
supportsPromptCache: false,
25+
}
26+
27+
const zooModels = { "anthropic/claude-sonnet-4": modelInfo }
28+
29+
// Test fixtures intentionally carry a single provider key; RouterModels requires
30+
// every provider key, so cast through unknown for these partial literals.
31+
const asRouterModels = (value: Record<string, Record<string, ModelInfo>>) => value as unknown as RouterModels
32+
33+
const setAuthenticated = (zooCodeIsAuthenticated: boolean) => {
34+
mockUseExtensionState.mockReturnValue({ zooCodeIsAuthenticated })
35+
}
36+
37+
const renderSyncHook = (queryClient: QueryClient) =>
38+
renderHook(() => useZooGatewayRouterModelsSync(), {
39+
wrapper: ({ children }: { children: React.ReactNode }) =>
40+
React.createElement(QueryClientProvider, { client: queryClient }, children),
41+
})
42+
43+
const makeQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false } } })
44+
45+
beforeEach(() => {
46+
vi.clearAllMocks()
47+
mockFetchRouterModels.mockResolvedValue(asRouterModels({ "zoo-gateway": zooModels }))
48+
})
49+
50+
describe("useZooGatewayRouterModelsSync", () => {
51+
it("does not fetch when the user is not authenticated", async () => {
52+
setAuthenticated(false)
53+
const queryClient = makeQueryClient()
54+
55+
renderSyncHook(queryClient)
56+
window.dispatchEvent(new MessageEvent("message", { data: { type: "zooGatewayCredentialsReady" } }))
57+
58+
await Promise.resolve()
59+
expect(mockFetchRouterModels).not.toHaveBeenCalled()
60+
})
61+
62+
it("fetches zoo-gateway models on the zooGatewayCredentialsReady message when authenticated", async () => {
63+
setAuthenticated(true)
64+
const queryClient = makeQueryClient()
65+
66+
renderSyncHook(queryClient)
67+
window.dispatchEvent(new MessageEvent("message", { data: { type: "zooGatewayCredentialsReady" } }))
68+
69+
await waitFor(() => expect(mockFetchRouterModels).toHaveBeenCalledWith("zoo-gateway"))
70+
})
71+
72+
it("fetches once on the false -> true authentication transition", async () => {
73+
setAuthenticated(false)
74+
const queryClient = makeQueryClient()
75+
76+
const { rerender } = renderSyncHook(queryClient)
77+
expect(mockFetchRouterModels).not.toHaveBeenCalled()
78+
79+
setAuthenticated(true)
80+
rerender()
81+
82+
await waitFor(() => expect(mockFetchRouterModels).toHaveBeenCalledTimes(1))
83+
})
84+
85+
it("merges into the routerModels cache without clobbering other providers", async () => {
86+
setAuthenticated(true)
87+
const queryClient = makeQueryClient()
88+
const existingOpenrouter = { "openai/gpt-4": modelInfo }
89+
queryClient.setQueryData(["routerModels", "all"], asRouterModels({ openrouter: existingOpenrouter }))
90+
91+
renderSyncHook(queryClient)
92+
window.dispatchEvent(new MessageEvent("message", { data: { type: "zooGatewayCredentialsReady" } }))
93+
94+
await waitFor(() => {
95+
const cached = queryClient.getQueryData<RouterModels>(["routerModels", "all"])
96+
expect(cached?.["zoo-gateway"]).toEqual(zooModels)
97+
expect(cached?.openrouter).toEqual(existingOpenrouter)
98+
})
99+
})
100+
101+
it("does not overwrite the cache when the fetch returns no zoo-gateway models", async () => {
102+
setAuthenticated(true)
103+
mockFetchRouterModels.mockResolvedValue(asRouterModels({ "zoo-gateway": {} }))
104+
const queryClient = makeQueryClient()
105+
queryClient.setQueryData(["routerModels", "all"], asRouterModels({ openrouter: { "openai/gpt-4": modelInfo } }))
106+
107+
renderSyncHook(queryClient)
108+
window.dispatchEvent(new MessageEvent("message", { data: { type: "zooGatewayCredentialsReady" } }))
109+
110+
await waitFor(() => expect(mockFetchRouterModels).toHaveBeenCalled())
111+
const cached = queryClient.getQueryData<RouterModels>(["routerModels", "all"])
112+
expect(cached?.["zoo-gateway"]).toBeUndefined()
113+
})
114+
115+
it("swallows fetch errors", async () => {
116+
setAuthenticated(true)
117+
mockFetchRouterModels.mockRejectedValue(new Error("router fetch in flight"))
118+
const queryClient = makeQueryClient()
119+
120+
renderSyncHook(queryClient)
121+
expect(() =>
122+
window.dispatchEvent(new MessageEvent("message", { data: { type: "zooGatewayCredentialsReady" } })),
123+
).not.toThrow()
124+
125+
await waitFor(() => expect(mockFetchRouterModels).toHaveBeenCalled())
126+
})
127+
})

0 commit comments

Comments
 (0)