Skip to content

Commit 13ecf4c

Browse files
committed
test: validate models, model picker
1 parent fef46c9 commit 13ecf4c

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed

webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,106 @@ describe("ModelPicker", () => {
136136
// Verify the API config was updated with the custom model ID
137137
expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelIdKey, customModelId)
138138
})
139+
140+
describe("Error Message Display", () => {
141+
it("displays error message when errorMessage prop is provided", async () => {
142+
const errorMessage = "Model not available for your organization"
143+
const propsWithError = {
144+
...defaultProps,
145+
errorMessage,
146+
}
147+
148+
await act(async () => {
149+
render(
150+
<QueryClientProvider client={queryClient}>
151+
<ModelPicker {...propsWithError} />
152+
</QueryClientProvider>,
153+
)
154+
})
155+
156+
// Check that the error message is displayed
157+
expect(screen.getByText(errorMessage)).toBeInTheDocument()
158+
})
159+
160+
it("does not display error message when errorMessage prop is undefined", async () => {
161+
await act(async () => renderModelPicker())
162+
163+
// Check that no error message is displayed
164+
expect(screen.queryByTestId("api-error-message")).not.toBeInTheDocument()
165+
})
166+
167+
it("displays error message below the model selector", async () => {
168+
const errorMessage = "Invalid model selected"
169+
const propsWithError = {
170+
...defaultProps,
171+
errorMessage,
172+
}
173+
174+
await act(async () => {
175+
render(
176+
<QueryClientProvider client={queryClient}>
177+
<ModelPicker {...propsWithError} />
178+
</QueryClientProvider>,
179+
)
180+
})
181+
182+
// Check that both the model selector and error message are present
183+
const modelSelector = screen.getByRole("combobox")
184+
const errorElement = screen.getByText(errorMessage)
185+
186+
expect(modelSelector).toBeInTheDocument()
187+
expect(errorElement).toBeInTheDocument()
188+
189+
// Verify the error message is rendered (positioning is handled by CSS)
190+
expect(errorElement).toBeVisible()
191+
})
192+
193+
it("updates error message when errorMessage prop changes", async () => {
194+
const initialError = "Initial error"
195+
const updatedError = "Updated error"
196+
197+
const { rerender } = render(
198+
<QueryClientProvider client={queryClient}>
199+
<ModelPicker {...defaultProps} errorMessage={initialError} />
200+
</QueryClientProvider>,
201+
)
202+
203+
// Check initial error is displayed
204+
expect(screen.getByText(initialError)).toBeInTheDocument()
205+
206+
// Update the error message
207+
rerender(
208+
<QueryClientProvider client={queryClient}>
209+
<ModelPicker {...defaultProps} errorMessage={updatedError} />
210+
</QueryClientProvider>,
211+
)
212+
213+
// Check that the error message has been updated
214+
expect(screen.queryByText(initialError)).not.toBeInTheDocument()
215+
expect(screen.getByText(updatedError)).toBeInTheDocument()
216+
})
217+
218+
it("removes error message when errorMessage prop becomes undefined", async () => {
219+
const errorMessage = "Temporary error"
220+
221+
const { rerender } = render(
222+
<QueryClientProvider client={queryClient}>
223+
<ModelPicker {...defaultProps} errorMessage={errorMessage} />
224+
</QueryClientProvider>,
225+
)
226+
227+
// Check error is initially displayed
228+
expect(screen.getByText(errorMessage)).toBeInTheDocument()
229+
230+
// Remove the error message
231+
rerender(
232+
<QueryClientProvider client={queryClient}>
233+
<ModelPicker {...defaultProps} errorMessage={undefined} />
234+
</QueryClientProvider>,
235+
)
236+
237+
// Check that the error message has been removed
238+
expect(screen.queryByText(errorMessage)).not.toBeInTheDocument()
239+
})
240+
})
139241
})
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
2+
import { RouterModels } from "@roo/api"
3+
4+
import { getModelValidationError, validateApiConfigurationExcludingModelErrors } from "../validate"
5+
6+
describe("Model Validation Functions", () => {
7+
const mockRouterModels: RouterModels = {
8+
openrouter: {
9+
"valid-model": {
10+
maxTokens: 8192,
11+
contextWindow: 200000,
12+
supportsImages: true,
13+
supportsPromptCache: false,
14+
inputPrice: 3.0,
15+
outputPrice: 15.0,
16+
},
17+
"another-valid-model": {
18+
maxTokens: 4096,
19+
contextWindow: 100000,
20+
supportsImages: false,
21+
supportsPromptCache: false,
22+
inputPrice: 1.0,
23+
outputPrice: 5.0,
24+
},
25+
},
26+
glama: {
27+
"valid-model": {
28+
maxTokens: 8192,
29+
contextWindow: 200000,
30+
supportsImages: true,
31+
supportsPromptCache: false,
32+
inputPrice: 3.0,
33+
outputPrice: 15.0,
34+
},
35+
},
36+
requesty: {},
37+
unbound: {},
38+
litellm: {},
39+
}
40+
41+
const allowAllOrganization: OrganizationAllowList = {
42+
allowAll: true,
43+
providers: {},
44+
}
45+
46+
const restrictiveOrganization: OrganizationAllowList = {
47+
allowAll: false,
48+
providers: {
49+
openrouter: {
50+
allowAll: false,
51+
models: ["valid-model"],
52+
},
53+
},
54+
}
55+
56+
describe("getModelValidationError", () => {
57+
it("returns undefined for valid OpenRouter model", () => {
58+
const config: ProviderSettings = {
59+
apiProvider: "openrouter",
60+
openRouterModelId: "valid-model",
61+
}
62+
63+
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
64+
expect(result).toBeUndefined()
65+
})
66+
67+
it("returns error for invalid OpenRouter model", () => {
68+
const config: ProviderSettings = {
69+
apiProvider: "openrouter",
70+
openRouterModelId: "invalid-model",
71+
}
72+
73+
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
74+
expect(result).toBe("validation.modelAvailability")
75+
})
76+
77+
it("returns error for model not allowed by organization", () => {
78+
const config: ProviderSettings = {
79+
apiProvider: "openrouter",
80+
openRouterModelId: "another-valid-model",
81+
}
82+
83+
const result = getModelValidationError(config, mockRouterModels, restrictiveOrganization)
84+
expect(result).toContain("model")
85+
})
86+
87+
it("returns undefined for valid Glama model", () => {
88+
const config: ProviderSettings = {
89+
apiProvider: "glama",
90+
glamaModelId: "valid-model",
91+
}
92+
93+
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
94+
expect(result).toBeUndefined()
95+
})
96+
97+
it("returns error for invalid Glama model", () => {
98+
const config: ProviderSettings = {
99+
apiProvider: "glama",
100+
glamaModelId: "invalid-model",
101+
}
102+
103+
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
104+
expect(result).toBeUndefined()
105+
})
106+
107+
it("returns undefined for OpenAI models when no router models provided", () => {
108+
const config: ProviderSettings = {
109+
apiProvider: "openai",
110+
openAiModelId: "gpt-4",
111+
}
112+
113+
const result = getModelValidationError(config, undefined, allowAllOrganization)
114+
expect(result).toBeUndefined()
115+
})
116+
117+
it("handles empty model IDs gracefully", () => {
118+
const config: ProviderSettings = {
119+
apiProvider: "openrouter",
120+
openRouterModelId: "",
121+
}
122+
123+
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
124+
expect(result).toBe("validation.modelId")
125+
})
126+
127+
it("handles undefined model IDs gracefully", () => {
128+
const config: ProviderSettings = {
129+
apiProvider: "openrouter",
130+
// openRouterModelId is undefined
131+
}
132+
133+
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
134+
expect(result).toBe("validation.modelId")
135+
})
136+
})
137+
138+
describe("validateApiConfigurationExcludingModelErrors", () => {
139+
it("returns undefined when configuration is valid", () => {
140+
const config: ProviderSettings = {
141+
apiProvider: "openrouter",
142+
openRouterApiKey: "valid-key",
143+
openRouterModelId: "valid-model",
144+
}
145+
146+
const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
147+
expect(result).toBeUndefined()
148+
})
149+
150+
it("returns error for missing API key", () => {
151+
const config: ProviderSettings = {
152+
apiProvider: "openrouter",
153+
openRouterModelId: "valid-model",
154+
// Missing openRouterApiKey
155+
}
156+
157+
const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
158+
expect(result).toBe("validation.apiKey")
159+
})
160+
161+
it("excludes model-specific errors", () => {
162+
const config: ProviderSettings = {
163+
apiProvider: "openrouter",
164+
openRouterApiKey: "valid-key",
165+
openRouterModelId: "invalid-model", // This should be ignored
166+
}
167+
168+
const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
169+
expect(result).toBeUndefined() // Should not return model validation error
170+
})
171+
172+
it("excludes model-specific organization errors", () => {
173+
const config: ProviderSettings = {
174+
apiProvider: "openrouter",
175+
openRouterApiKey: "valid-key",
176+
openRouterModelId: "another-valid-model", // Not allowed by restrictive org
177+
}
178+
179+
const result = validateApiConfigurationExcludingModelErrors(
180+
config,
181+
mockRouterModels,
182+
restrictiveOrganization,
183+
)
184+
expect(result).toBeUndefined() // Should exclude model-specific org errors
185+
})
186+
})
187+
})

0 commit comments

Comments
 (0)