mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Implement tests for LiteLLM component and enhance API configuration handling
- Added comprehensive tests for the LiteLLM component to validate API key and base URL handling during model refresh. - Updated the LiteLLMProps type to be exported for better accessibility. - Enhanced the SettingsView tests to ensure correct rendering and default values for LiteLLM configuration. - Mocked ApiOptions in tests to inspect props and validate API configuration behavior.
This commit is contained in:
parent
f92e4b9233
commit
3053678903
4 changed files with 266 additions and 4 deletions
|
|
@ -41,8 +41,8 @@ jest.mock("vscrui", () => ({
|
|||
|
||||
// Mock @shadcn/ui components
|
||||
jest.mock("@/components/ui", () => ({
|
||||
Select: ({ children, value, onValueChange }: any) => (
|
||||
<div className="select-mock">
|
||||
Select: ({ children, value, onValueChange, ...rest }: any) => (
|
||||
<div className="select-mock" data-testid={rest["data-testid"]}>
|
||||
<select value={value} onChange={(e) => onValueChange && onValueChange(e.target.value)}>
|
||||
{children}
|
||||
</select>
|
||||
|
|
@ -152,11 +152,13 @@ jest.mock("@src/components/ui/hooks/useSelectedModel", () => ({
|
|||
if (apiConfiguration.apiModelId?.includes("thinking")) {
|
||||
return {
|
||||
provider: apiConfiguration.apiProvider,
|
||||
id: apiConfiguration.apiModelId,
|
||||
info: { thinking: true, contextWindow: 4000, maxTokens: 128000 },
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
provider: apiConfiguration.apiProvider,
|
||||
id: apiConfiguration.apiModelId,
|
||||
info: { contextWindow: 4000 },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,14 @@ jest.mock("@/components/ui", () => ({
|
|||
),
|
||||
}))
|
||||
|
||||
// Mock ApiOptions to inspect its props
|
||||
jest.mock("../ApiOptions", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((props) => (
|
||||
<div data-testid="api-options-mock" data-apiconfiguration={JSON.stringify(props.apiConfiguration)} />
|
||||
)),
|
||||
}))
|
||||
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
const mockPostMessage = (state: any) => {
|
||||
window.postMessage(
|
||||
|
|
@ -369,13 +377,87 @@ describe("SettingsView - Sound Settings", () => {
|
|||
describe("SettingsView - API Configuration", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
// Reset ApiOptions mock calls before each test if needed
|
||||
require("../ApiOptions").default.mockClear()
|
||||
})
|
||||
|
||||
it("renders ApiConfigManagement with correct props", () => {
|
||||
it("renders ApiConfigManager with correct props", () => {
|
||||
renderSettingsView()
|
||||
|
||||
expect(screen.getByTestId("api-config-management")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("defaults LiteLLM fields in apiConfiguration if provider is litellm and fields are missing", async () => {
|
||||
const initialExtensionState = {
|
||||
apiConfiguration: {
|
||||
apiProvider: "litellm",
|
||||
// litellmBaseUrl, litellmApiKey, litellmModelId are missing
|
||||
},
|
||||
// Add other necessary state fields for useExtensionState mock
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
uriScheme: "vscode",
|
||||
version: "1.0.0",
|
||||
settingsImportedAt: null,
|
||||
}
|
||||
|
||||
// Mock useExtensionState to return our initial state
|
||||
const mockUseExtensionState = jest.spyOn(require("@/context/ExtensionStateContext"), "useExtensionState")
|
||||
mockUseExtensionState.mockReturnValue(initialExtensionState)
|
||||
|
||||
const { activateTab } = renderSettingsView() // onDone is part of the return, but we don't need it here
|
||||
|
||||
// Ensure providers tab is active (it should be by default, but explicit doesn't hurt)
|
||||
activateTab("providers")
|
||||
|
||||
// Wait for effects to run. Finding the mocked ApiOptions is a good way to ensure it has rendered with updated props.
|
||||
const apiOptionsMock = await screen.findByTestId("api-options-mock")
|
||||
const passedApiConfigString = apiOptionsMock.getAttribute("data-apiconfiguration")
|
||||
const passedApiConfig = JSON.parse(passedApiConfigString!)
|
||||
|
||||
expect(passedApiConfig.apiProvider).toBe("litellm")
|
||||
expect(passedApiConfig.litellmBaseUrl).toBe("http://localhost:4000")
|
||||
expect(passedApiConfig.litellmApiKey).toBe("sk-1234")
|
||||
expect(passedApiConfig.litellmModelId).toBeDefined() // Check it's defined (actual value is litellmDefaultModelId)
|
||||
|
||||
mockUseExtensionState.mockRestore()
|
||||
})
|
||||
|
||||
it("preserves existing LiteLLM fields in apiConfiguration if provider is litellm", async () => {
|
||||
const myCustomKey = "my-custom-key"
|
||||
const myCustomUrl = "http://my-custom-url.com"
|
||||
const myCustomModel = "custom-model/my-model"
|
||||
const initialExtensionState = {
|
||||
apiConfiguration: {
|
||||
apiProvider: "litellm",
|
||||
litellmBaseUrl: myCustomUrl,
|
||||
litellmApiKey: myCustomKey,
|
||||
litellmModelId: myCustomModel,
|
||||
},
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
uriScheme: "vscode",
|
||||
version: "1.0.0",
|
||||
settingsImportedAt: null,
|
||||
}
|
||||
|
||||
const mockUseExtensionState = jest.spyOn(require("@/context/ExtensionStateContext"), "useExtensionState")
|
||||
mockUseExtensionState.mockReturnValue(initialExtensionState)
|
||||
|
||||
const { activateTab } = renderSettingsView()
|
||||
activateTab("providers")
|
||||
|
||||
const apiOptionsMock = await screen.findByTestId("api-options-mock")
|
||||
const passedApiConfigString = apiOptionsMock.getAttribute("data-apiconfiguration")
|
||||
const passedApiConfig = JSON.parse(passedApiConfigString!)
|
||||
|
||||
expect(passedApiConfig.apiProvider).toBe("litellm")
|
||||
expect(passedApiConfig.litellmBaseUrl).toBe(myCustomUrl)
|
||||
expect(passedApiConfig.litellmApiKey).toBe(myCustomKey)
|
||||
expect(passedApiConfig.litellmModelId).toBe(myCustomModel)
|
||||
|
||||
mockUseExtensionState.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("SettingsView - Allowed Commands", () => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ModelPicker } from "../ModelPicker"
|
|||
import { WebviewMessage } from "@roo/shared/WebviewMessage"
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
|
||||
type LiteLLMProps = {
|
||||
export type LiteLLMProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
// routerModels prop might need to be updated by parent if we want to show new models immediately.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import React from "react"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { I18nextProvider } from "react-i18next"
|
||||
import i18next from "i18next"
|
||||
|
||||
import { LiteLLM, LiteLLMProps } from "../LiteLLM"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
// Minimal i18n instance for testing
|
||||
const testI18n = i18next.createInstance()
|
||||
testI18n.init({
|
||||
fallbackLng: "en",
|
||||
debug: false,
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
"settings:providers.refreshModels.label": "Refresh Models",
|
||||
"settings:providers.refreshModels.missingConfig": "API key or base URL missing.",
|
||||
},
|
||||
},
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false, // Not needed for React
|
||||
},
|
||||
})
|
||||
|
||||
// Mock vscode API
|
||||
jest.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock VSCodeTextField
|
||||
jest.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeTextField: ({ children, value, onInput, type }: any) => (
|
||||
<div>
|
||||
{children}
|
||||
<input
|
||||
type={type || "text"}
|
||||
value={value}
|
||||
onChange={(e) => onInput && onInput({ target: { value: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock ModelPicker
|
||||
jest.mock("../../ModelPicker", () => ({
|
||||
ModelPicker: () => <div data-testid="model-picker-mock">ModelPicker</div>,
|
||||
}))
|
||||
|
||||
const mockT = jest.fn((key) => key) // Simple t mock
|
||||
|
||||
jest.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: mockT,
|
||||
}),
|
||||
}))
|
||||
|
||||
const defaultProps: LiteLLMProps = {
|
||||
apiConfiguration: { litellmApiKey: "", litellmBaseUrl: "" },
|
||||
setApiConfigurationField: jest.fn(),
|
||||
routerModels: {
|
||||
litellm: {},
|
||||
glama: {},
|
||||
openrouter: {},
|
||||
unbound: {},
|
||||
requesty: {},
|
||||
},
|
||||
}
|
||||
|
||||
const renderLiteLLM = (props?: Partial<LiteLLMProps>) => {
|
||||
return render(
|
||||
<I18nextProvider i18n={testI18n}>
|
||||
<LiteLLM {...defaultProps} {...props} />
|
||||
</I18nextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("LiteLLM Component", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
// Reset the ref module-level if needed, but usually refs are instance-based.
|
||||
// For this test, we rely on fresh mounts giving fresh refs.
|
||||
})
|
||||
|
||||
it("does not attempt initial model refresh if API key is missing", () => {
|
||||
renderLiteLLM({
|
||||
apiConfiguration: {
|
||||
...defaultProps.apiConfiguration,
|
||||
litellmBaseUrl: "http://localhost:4000",
|
||||
litellmApiKey: "",
|
||||
},
|
||||
})
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "requestProviderModels" }))
|
||||
})
|
||||
|
||||
it("does not attempt initial model refresh if base URL is missing", () => {
|
||||
renderLiteLLM({
|
||||
apiConfiguration: { ...defaultProps.apiConfiguration, litellmApiKey: "test-key", litellmBaseUrl: "" },
|
||||
})
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "requestProviderModels" }))
|
||||
})
|
||||
|
||||
it("attempts initial model refresh once if API key and base URL are present on mount", () => {
|
||||
renderLiteLLM({
|
||||
apiConfiguration: {
|
||||
...defaultProps.apiConfiguration,
|
||||
litellmApiKey: "test-key",
|
||||
litellmBaseUrl: "http://localhost:4000",
|
||||
},
|
||||
})
|
||||
expect(vscode.postMessage).toHaveBeenCalledTimes(1)
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "requestProviderModels",
|
||||
payload: {
|
||||
provider: "litellm",
|
||||
apiKey: "test-key",
|
||||
baseUrl: "http://localhost:4000",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("does not re-attempt initial refresh if props change but refresh was already done", () => {
|
||||
const { rerender } = renderLiteLLM({
|
||||
apiConfiguration: {
|
||||
...defaultProps.apiConfiguration,
|
||||
litellmApiKey: "test-key",
|
||||
litellmBaseUrl: "http://localhost:4000",
|
||||
},
|
||||
})
|
||||
expect(vscode.postMessage).toHaveBeenCalledTimes(1) // Initial call
|
||||
|
||||
// Re-render with different routerModels (a prop that might change)
|
||||
rerender(
|
||||
<I18nextProvider i18n={testI18n}>
|
||||
<LiteLLM
|
||||
{...defaultProps}
|
||||
apiConfiguration={{
|
||||
...defaultProps.apiConfiguration,
|
||||
litellmApiKey: "test-key", // Same key
|
||||
litellmBaseUrl: "http://localhost:4000", // Same URL
|
||||
}}
|
||||
routerModels={{
|
||||
litellm: { "new-model": { contextWindow: 4096, supportsPromptCache: false } },
|
||||
glama: {},
|
||||
openrouter: {},
|
||||
unbound: {},
|
||||
requesty: {},
|
||||
}}
|
||||
/>
|
||||
</I18nextProvider>,
|
||||
)
|
||||
// Should still only be 1 call from the initial refresh
|
||||
expect(vscode.postMessage).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("manual refresh button is disabled if API key is missing", () => {
|
||||
renderLiteLLM({
|
||||
apiConfiguration: {
|
||||
...defaultProps.apiConfiguration,
|
||||
litellmBaseUrl: "http://localhost:4000",
|
||||
litellmApiKey: "",
|
||||
},
|
||||
})
|
||||
const refreshButton = screen.getByText("settings:providers.refreshModels.label").closest("button")
|
||||
expect(refreshButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("manual refresh button is disabled if base URL is missing", () => {
|
||||
renderLiteLLM({
|
||||
apiConfiguration: { ...defaultProps.apiConfiguration, litellmApiKey: "test-key", litellmBaseUrl: "" },
|
||||
})
|
||||
const refreshButton = screen.getByText("settings:providers.refreshModels.label").closest("button")
|
||||
expect(refreshButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue