Skip to content

Commit 983c133

Browse files
James Mtendamemacursoragent
andcommitted
test(zoo-gateway): cover dynamic default model picker for codecov patch
Exports pickZooGatewayDefaultModelId so the helper is unit-testable and adds component tests for the auto-default useEffect (no-op while catalog loads, auto-pick on empty profile, repair stale id, no-op when valid). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8877ec7 commit 983c133

2 files changed

Lines changed: 170 additions & 1 deletion

File tree

webview-ui/src/components/settings/providers/ZooGateway.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ function isSonnet45ModelId(id: string) {
2525
return /sonnet-4[.-]5|sonnet-4\.5/i.test(id)
2626
}
2727

28-
function pickZooGatewayDefaultModelId(modelIds: string[]) {
28+
// Exported for unit tests.
29+
export function pickZooGatewayDefaultModelId(modelIds: string[]) {
2930
if (modelIds.length === 0) {
3031
return zooGatewayDefaultModelId
3132
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import React from "react"
2+
import { render, waitFor } from "@/utils/test-utils"
3+
import type { ModelInfo, ProviderSettings, RouterModels } from "@roo-code/types"
4+
5+
import { ZooGateway, pickZooGatewayDefaultModelId } from "../ZooGateway"
6+
7+
vi.mock("@src/i18n/TranslationContext", () => ({
8+
useAppTranslation: () => ({
9+
t: (key: string) => key,
10+
}),
11+
}))
12+
13+
vi.mock("@src/context/ExtensionStateContext", () => ({
14+
useExtensionState: () => ({
15+
zooCodeIsAuthenticated: true,
16+
zooCodeUserEmail: "user@example.com",
17+
zooCodeUserName: "User",
18+
zooCodeBaseUrl: "https://www.zoocode.dev",
19+
uriScheme: "vscode",
20+
deviceName: "Test Device",
21+
}),
22+
}))
23+
24+
vi.mock("@src/oauth/urls", () => ({
25+
getZooCodeAuthUrl: () => "https://www.zoocode.dev/dashboard/connect",
26+
}))
27+
28+
vi.mock("../../ModelPicker", () => ({
29+
ModelPicker: ({ defaultModelId }: { defaultModelId: string }) => (
30+
<div data-testid="model-picker" data-default-model={defaultModelId} />
31+
),
32+
}))
33+
34+
const baseInfo: ModelInfo = {
35+
maxTokens: 8192,
36+
contextWindow: 200000,
37+
supportsImages: false,
38+
supportsPromptCache: false,
39+
inputPrice: 1,
40+
outputPrice: 2,
41+
}
42+
43+
function buildRouterModels(modelIds: string[]): RouterModels {
44+
const models = Object.fromEntries(modelIds.map((id) => [id, baseInfo]))
45+
return { "zoo-gateway": models } as unknown as RouterModels
46+
}
47+
48+
describe("pickZooGatewayDefaultModelId", () => {
49+
it("falls back to the static default when the catalog is empty", () => {
50+
expect(pickZooGatewayDefaultModelId([])).toBe("anthropic/claude-sonnet-4")
51+
})
52+
53+
it("prefers an exact anthropic/claude-sonnet-4.5 match", () => {
54+
const result = pickZooGatewayDefaultModelId([
55+
"anthropic/claude-sonnet-4",
56+
"anthropic/claude-sonnet-4.5",
57+
"openai/gpt-4o",
58+
])
59+
expect(result).toBe("anthropic/claude-sonnet-4.5")
60+
})
61+
62+
it("matches a Bedrock-style claude-sonnet-4-5 id", () => {
63+
const result = pickZooGatewayDefaultModelId([
64+
"anthropic.claude-sonnet-4-20250514-v1:0",
65+
"anthropic.claude-sonnet-4-5-20250929-v1:0",
66+
])
67+
expect(result).toBe("anthropic.claude-sonnet-4-5-20250929-v1:0")
68+
})
69+
70+
it("falls back to claude sonnet 4 when 4.5 is not in the catalog", () => {
71+
const result = pickZooGatewayDefaultModelId(["openai/gpt-4o", "anthropic/claude-sonnet-4"])
72+
expect(result).toBe("anthropic/claude-sonnet-4")
73+
})
74+
75+
it("falls back to the first available id when no claude sonnet is present", () => {
76+
const result = pickZooGatewayDefaultModelId(["openai/gpt-4o", "google/gemini-2.5-pro"])
77+
expect(result).toBe("openai/gpt-4o")
78+
})
79+
})
80+
81+
describe("ZooGateway component", () => {
82+
const baseProps = {
83+
organizationAllowList: { allowAll: true, providers: {} } as ProviderSettings extends never ? never : any,
84+
setApiConfigurationField: vi.fn(),
85+
}
86+
87+
beforeEach(() => {
88+
vi.clearAllMocks()
89+
})
90+
91+
it("auto-selects the resolved default model when the profile has no model id", async () => {
92+
const setApiConfigurationField = vi.fn()
93+
render(
94+
<ZooGateway
95+
apiConfiguration={{ apiProvider: "zoo-gateway" } as ProviderSettings}
96+
setApiConfigurationField={setApiConfigurationField}
97+
routerModels={buildRouterModels(["anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5"])}
98+
organizationAllowList={baseProps.organizationAllowList}
99+
/>,
100+
)
101+
102+
await waitFor(() => {
103+
expect(setApiConfigurationField).toHaveBeenCalledWith("zooGatewayModelId", "anthropic/claude-sonnet-4.5")
104+
})
105+
})
106+
107+
it("reassigns a stale model id that is not in the catalog", async () => {
108+
const setApiConfigurationField = vi.fn()
109+
render(
110+
<ZooGateway
111+
apiConfiguration={
112+
{
113+
apiProvider: "zoo-gateway",
114+
zooGatewayModelId: "anthropic/claude-sonnet-4",
115+
} as ProviderSettings
116+
}
117+
setApiConfigurationField={setApiConfigurationField}
118+
routerModels={buildRouterModels([
119+
"anthropic.claude-sonnet-4-5-20250929-v1:0",
120+
"anthropic.claude-sonnet-4-20250514-v1:0",
121+
])}
122+
organizationAllowList={baseProps.organizationAllowList}
123+
/>,
124+
)
125+
126+
await waitFor(() => {
127+
expect(setApiConfigurationField).toHaveBeenCalledWith(
128+
"zooGatewayModelId",
129+
"anthropic.claude-sonnet-4-5-20250929-v1:0",
130+
)
131+
})
132+
})
133+
134+
it("does not overwrite a model id that is already valid for the catalog", async () => {
135+
const setApiConfigurationField = vi.fn()
136+
render(
137+
<ZooGateway
138+
apiConfiguration={
139+
{
140+
apiProvider: "zoo-gateway",
141+
zooGatewayModelId: "anthropic/claude-sonnet-4.5",
142+
} as ProviderSettings
143+
}
144+
setApiConfigurationField={setApiConfigurationField}
145+
routerModels={buildRouterModels(["anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5"])}
146+
organizationAllowList={baseProps.organizationAllowList}
147+
/>,
148+
)
149+
150+
await waitFor(() => {
151+
expect(setApiConfigurationField).not.toHaveBeenCalled()
152+
})
153+
})
154+
155+
it("does nothing while the catalog is still empty (router models loading)", () => {
156+
const setApiConfigurationField = vi.fn()
157+
render(
158+
<ZooGateway
159+
apiConfiguration={{ apiProvider: "zoo-gateway" } as ProviderSettings}
160+
setApiConfigurationField={setApiConfigurationField}
161+
routerModels={undefined}
162+
organizationAllowList={baseProps.organizationAllowList}
163+
/>,
164+
)
165+
166+
expect(setApiConfigurationField).not.toHaveBeenCalled()
167+
})
168+
})

0 commit comments

Comments
 (0)