Skip to content

Commit 8254e11

Browse files
authored
fix(settings): pin Zoo Gateway provider first (#2419)
* fix(settings): pin Zoo Gateway provider first * test(settings): strengthen provider ordering coverage
1 parent 18861b2 commit 8254e11

5 files changed

Lines changed: 113 additions & 0 deletions

File tree

‎webview-ui/playwright/gallery/stories.tsx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,32 @@ export const stories: Record<string, Story> = {
146146
</AppProviders>
147147
)
148148
},
149+
"api-provider-dropdown": async () => {
150+
const [{ AppProviders }, { default: ApiOptions }, { providerIdentifiers }] = await Promise.all([
151+
import("../AppProviders"),
152+
import("@/components/settings/ApiOptions"),
153+
import("@roo-code/types"),
154+
])
155+
const apiConfiguration: import("@roo-code/types").ProviderSettings = {
156+
apiProvider: providerIdentifiers.anthropic,
157+
apiKey: "sk-ant-example-key",
158+
}
159+
return (
160+
<AppProviders initialState={{ apiConfiguration }}>
161+
<div
162+
className="w-[480px] p-4 bg-vscode-editor-background text-vscode-foreground"
163+
data-testid="api-provider-dropdown-story">
164+
<ApiOptions
165+
uriScheme="vscode"
166+
apiConfiguration={apiConfiguration}
167+
setApiConfigurationField={() => undefined}
168+
errorMessage={undefined}
169+
setErrorMessage={() => undefined}
170+
/>
171+
</div>
172+
</AppProviders>
173+
)
174+
},
149175
"auto-approve-settings": async () => {
150176
const { AutoApproveSettingsStory } =
151177
await import("@/components/settings/__tests__/AutoApproveSettings.visual.fixture")

‎webview-ui/src/components/settings/ApiOptions.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,12 @@ const ApiOptions = ({
435435
}
436436
}
437437

438+
const zooGatewayIndex = options.findIndex((opt) => opt.value === providerIdentifiers.zooGateway)
439+
if (zooGatewayIndex > 0) {
440+
const [zooGatewayOption] = options.splice(zooGatewayIndex, 1)
441+
options.unshift(zooGatewayOption)
442+
}
443+
438444
return options
439445
}, [organizationAllowList, apiConfiguration.apiProvider, fromWelcomeView])
440446

‎webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
1111
import ApiOptions from "../ApiOptions"
1212
import { MODELS_BY_PROVIDER, PROVIDERS } from "../constants"
1313

14+
type ProviderOption = {
15+
value: string
16+
label: string
17+
}
18+
1419
// Mock the extension state context
1520
vi.mock("@src/context/ExtensionStateContext", () => ({
1621
ExtensionStateContextProvider: ({ children }: any) => children,
@@ -113,6 +118,11 @@ describe("ApiOptions Provider Filtering", () => {
113118
return renderWithExtensionState(<ApiOptions {...props} />)
114119
}
115120

121+
const getProviderOptions = (): ProviderOption[] => {
122+
const selectElement = screen.getByTestId("provider-select")
123+
return JSON.parse(selectElement.getAttribute("data-options") || "[]") as ProviderOption[]
124+
}
125+
116126
beforeEach(() => {
117127
vi.clearAllMocks()
118128
vi.mocked(useSelectedModel).mockReturnValue({
@@ -190,6 +200,64 @@ describe("ApiOptions Provider Filtering", () => {
190200
expect(providerValues).toContain("ollama") // dynamic provider
191201
})
192202

203+
it("pins Zoo Gateway first while preserving alphabetical order for the remaining providers", () => {
204+
renderWithProviders()
205+
206+
const options = getProviderOptions()
207+
const remainingLabels = options.slice(1).map(({ label }) => label)
208+
209+
expect(options[0]?.value).toBe(providerIdentifiers.zooGateway)
210+
expect(remainingLabels).toEqual([...remainingLabels].sort((a, b) => a.localeCompare(b)))
211+
})
212+
213+
it("pins Zoo Gateway first and OpenRouter second in the welcome view", () => {
214+
renderWithProviders({ ...defaultProps, fromWelcomeView: true })
215+
216+
const providerValues = getProviderOptions().map(({ value }) => value)
217+
218+
expect(providerValues.slice(0, 2)).toEqual([providerIdentifiers.zooGateway, providerIdentifiers.openrouter])
219+
})
220+
221+
it("does not reintroduce Zoo Gateway when an organization allow list excludes it", () => {
222+
vi.mocked(useExtensionState).mockReturnValueOnce({
223+
...useExtensionState(),
224+
organizationAllowList: {
225+
allowAll: false,
226+
providers: {
227+
[providerIdentifiers.anthropic]: { allowAll: true },
228+
[providerIdentifiers.openrouter]: { allowAll: true },
229+
},
230+
},
231+
})
232+
233+
renderWithProviders()
234+
235+
expect(getProviderOptions().map(({ value }) => value)).toEqual([
236+
providerIdentifiers.anthropic,
237+
providerIdentifiers.openrouter,
238+
])
239+
})
240+
241+
it("keeps Zoo Gateway first when an organization allow list includes it", () => {
242+
vi.mocked(useExtensionState).mockReturnValueOnce({
243+
...useExtensionState(),
244+
organizationAllowList: {
245+
allowAll: false,
246+
providers: {
247+
[providerIdentifiers.anthropic]: { allowAll: true },
248+
[providerIdentifiers.zooGateway]: { allowAll: true },
249+
},
250+
},
251+
})
252+
253+
renderWithProviders()
254+
255+
expect(getProviderOptions().map(({ value }) => value)).toEqual([
256+
providerIdentifiers.zooGateway,
257+
providerIdentifiers.anthropic,
258+
])
259+
})
260+
193261
it("should hide static providers with empty models", () => {
194262
// Mock MODELS_BY_PROVIDER to have an empty provider
195263
const _originalModels = { ...MODELS_BY_PROVIDER }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expect, test } from "../../../../playwright/coverage-fixture"
2+
import { mountedStory } from "../../../../playwright/mounted-story"
3+
4+
test("renders the open provider dropdown with Zoo Gateway first", async ({ mount, page }) => {
5+
await page.setViewportSize({ width: 520, height: 720 })
6+
// The full provider bundle leaves bare Zod references after gallery tree-shaking.
7+
await page.evaluate(() => Object.assign(globalThis, { z: undefined, z$1: undefined }))
8+
const component = mountedStory(await mount("api-provider-dropdown"))
9+
10+
await component.getByTestId("provider-select").click()
11+
await expect(page.getByRole("listbox")).toBeVisible()
12+
await expect(page).toHaveScreenshot("api-provider-dropdown-open.png")
13+
})
42 KB
Loading

0 commit comments

Comments
 (0)