onChange(parseFloat(e.target.value))}
+ value={apiConfiguration?.temperature || 0}
+ onChange={(e: any) => setApiConfigurationField("temperature", parseFloat(e.target.value))}
min={0}
max={2}
step={0.1}
@@ -107,381 +180,325 @@ jest.mock("../TemperatureControl", () => ({
jest.mock("../RateLimitSecondsControl", () => ({
RateLimitSecondsControl: ({ value, onChange }: any) => (
),
}))
-// Mock DiffSettingsControl for tests
-jest.mock("../DiffSettingsControl", () => ({
- DiffSettingsControl: ({ diffEnabled, fuzzyMatchThreshold, onChange }: any) => (
-
-
- Enable editing through diffs
- onChange("diffEnabled", e.target.checked)}
- />
-
-
- Fuzzy match threshold
- onChange("fuzzyMatchThreshold", parseFloat(e.target.value))}
- min={0.8}
- max={1}
- step={0.005}
- />
-
+jest.mock("../ModelInfoView", () => ({
+ ModelInfoView: ({ modelInfo, modelProvider }: any) => (
+
+ {modelInfo && {JSON.stringify(modelInfo)} }
+ {modelProvider && {modelProvider} }
),
}))
-// Mock ThinkingBudget component
+jest.mock("../ApiErrorMessage", () => ({
+ ApiErrorMessage: ({ error }: any) =>
{error && {error} }
,
+}))
+
jest.mock("../ThinkingBudget", () => ({
- ThinkingBudget: ({ modelInfo }: any) => {
- // Only render if model supports reasoning budget (thinking models)
- if (modelInfo?.supportsReasoningBudget || modelInfo?.requiredReasoningBudget) {
- return (
-
-
Max Thinking Tokens
-
-
- )
+ ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo }: any) => {
+ // Match the real component's logic
+ if (!modelInfo) return null
+
+ const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget
+ const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget
+ const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort
+ const enableReasoningEffort = apiConfiguration?.enableReasoningEffort
+
+ if (isReasoningBudgetSupported && !!modelInfo.maxTokens) {
+ // Only show if required OR if user has enabled it
+ if (isReasoningBudgetRequired || enableReasoningEffort) {
+ return (
+
+
+ setApiConfigurationField("modelMaxThinkingTokens", parseInt(e.target.value))
+ }
+ />
+
+ )
+ }
+ return null
+ } else if (isReasoningEffortSupported) {
+ return
}
+
return null
},
}))
-// Mock LiteLLM provider for tests
-jest.mock("../providers/LiteLLM", () => ({
- LiteLLM: ({ apiConfiguration, setApiConfigurationField }: any) => (
-
- setApiConfigurationField("litellmBaseUrl", e.target.value)}
- placeholder="Base URL"
- />
- setApiConfigurationField("litellmApiKey", e.target.value)}
- placeholder="API Key"
- />
- Refresh Models
-
- ),
+jest.mock("../MaxOutputTokensControl", () => ({
+ MaxOutputTokensControl: ({ apiConfiguration, setApiConfigurationField, modelInfo }: any) => {
+ // Only show if model has maxTokens > 0
+ if (!modelInfo || !modelInfo.maxTokens || modelInfo.maxTokens <= 0) {
+ return null
+ }
+ return (
+
+ Max Output Tokens
+ setApiConfigurationField("modelMaxTokens", parseInt(e.target.value))}
+ />
+
+ )
+ },
+}))
+
+jest.mock("../DiffSettingsControl", () => ({
+ DiffSettingsControl: () =>
,
+}))
+
+jest.mock("../providers", () => ({
+ Anthropic: () =>
,
+ Bedrock: () =>
,
+ Chutes: () =>
,
+ DeepSeek: () =>
,
+ Gemini: () =>
,
+ Glama: () =>
,
+ Groq: () =>
,
+ LMStudio: () =>
,
+ LiteLLM: () =>
,
+ Mistral: () =>
,
+ Ollama: () =>
,
+ OpenAI: () =>
,
+ OpenAICompatible: () =>
,
+ OpenRouter: () =>
,
+ Requesty: () =>
,
+ Unbound: () =>
,
+ Vertex: () =>
,
+ VSCodeLM: () =>
,
+ XAI: () =>
,
}))
jest.mock("@src/components/ui/hooks/useSelectedModel", () => ({
- useSelectedModel: jest.fn((apiConfiguration: ProviderSettings) => {
- if (apiConfiguration.apiModelId?.includes("thinking")) {
- const info: ModelInfo = {
- contextWindow: 4000,
- maxTokens: 128000,
- supportsPromptCache: true,
- requiredReasoningBudget: true,
+ useSelectedModel: (apiConfiguration: any) => {
+ const selectedModelId = apiConfiguration?.apiModelId
+ const selectedProvider = apiConfiguration?.apiProvider || "openai"
+
+ // Return thinking model for "thinking" models, non-thinking for others
+ let info = null
+ if (selectedModelId?.includes("thinking")) {
+ info = {
supportsReasoningBudget: true,
+ requiredReasoningBudget: false,
+ maxTokens: 16384,
+ contextWindow: 200000,
+ supportsPromptCache: true,
+ supportsImages: true,
}
-
- return {
- provider: apiConfiguration.apiProvider,
- info,
+ } else if (selectedModelId === "non-thinking-model-with-max-tokens") {
+ info = {
+ supportsReasoningBudget: false,
+ requiredReasoningBudget: false,
+ maxTokens: 8192,
+ contextWindow: 100000,
+ supportsPromptCache: true,
+ supportsImages: true,
}
- } else {
- const info: ModelInfo = { contextWindow: 4000, supportsPromptCache: true }
-
- return {
- provider: apiConfiguration.apiProvider,
- info,
+ } else if (selectedModelId === "model-without-max-tokens") {
+ info = {
+ supportsReasoningBudget: false,
+ requiredReasoningBudget: false,
+ maxTokens: 0,
+ contextWindow: 100000,
+ supportsPromptCache: true,
+ supportsImages: true,
+ }
+ } else if (selectedModelId === "gpt-4") {
+ // Default model
+ info = {
+ supportsReasoningBudget: false,
+ requiredReasoningBudget: false,
+ maxTokens: 8192,
+ contextWindow: 128000,
+ supportsPromptCache: true,
+ supportsImages: true,
}
}
+
+ return {
+ provider: selectedProvider,
+ id: selectedModelId,
+ info: info,
+ }
+ },
+}))
+
+jest.mock("react-use", () => ({
+ ...jest.requireActual("react-use"),
+ useDebounce: jest.fn(),
+ useEvent: jest.fn(),
+}))
+
+jest.mock("@src/utils/validate", () => ({
+ validateApiConfiguration: jest.fn(() => ({ isValid: true })),
+}))
+
+jest.mock("@src/utils/docLinks", () => ({
+ buildDocLink: jest.fn((path: string) => `https://docs.example.com/${path}`),
+}))
+
+jest.mock("@src/context/ExtensionStateContext", () => ({
+ ...jest.requireActual("@src/context/ExtensionStateContext"),
+ useExtensionState: () => ({
+ organizationAllowList: {
+ requiredProvidersForParsing: [],
+ organizationData: {},
+ providers: {
+ anthropic: { allowAll: true },
+ bedrock: { allowAll: true },
+ openai: { allowAll: true },
+ gemini: { allowAll: true },
+ ollama: { allowAll: true },
+ deepseek: { allowAll: true },
+ mistral: { allowAll: true },
+ vertex: { allowAll: true },
+ lmstudio: { allowAll: true },
+ "openai-native": { allowAll: true },
+ "vscode-lm": { allowAll: true },
+ xai: { allowAll: true },
+ groq: { allowAll: true },
+ chutes: { allowAll: true },
+ openrouter: { allowAll: true },
+ glama: { allowAll: true },
+ requesty: { allowAll: true },
+ unbound: { allowAll: true },
+ litellm: { allowAll: true },
+ },
+ },
}),
}))
+// Now the imports
+import { render, screen } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+
+import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
+
+import ApiOptions, { ApiOptionsProps } from "../ApiOptions"
+
+// Mock vscode object
+declare global {
+ // eslint-disable-next-line no-var
+ var vscode: {
+ postMessage: jest.Mock
+ }
+}
+global.vscode = {
+ postMessage: jest.fn(),
+}
+
const renderApiOptions = (props: Partial
= {}) => {
+ const defaultProps: ApiOptionsProps = {
+ uriScheme: undefined,
+ apiConfiguration: {
+ apiProvider: "openai",
+ apiModelId: "gpt-4",
+ },
+ setApiConfigurationField: jest.fn(),
+ fromWelcomeView: false,
+ errorMessage: undefined,
+ setErrorMessage: jest.fn(),
+ }
+
const queryClient = new QueryClient()
render(
- {}}
- uriScheme={undefined}
- apiConfiguration={{}}
- setApiConfigurationField={() => {}}
- {...props}
- />
+
,
)
}
-describe("ApiOptions", () => {
- it("shows diff settings, temperature and rate limit controls by default", () => {
+describe("ApiOptions Component", () => {
+ it("renders controls for non-thinking model with max tokens", () => {
renderApiOptions({
apiConfiguration: {
- diffEnabled: true,
- fuzzyMatchThreshold: 0.95,
+ apiProvider: "openai" as const,
+ apiModelId: "non-thinking-model-with-max-tokens",
+ enableReasoningEffort: false, // Explicitly set to false
},
})
- // Check for DiffSettingsControl by looking for text content
- expect(screen.getByText(/enable editing through diffs/i)).toBeInTheDocument()
- expect(screen.getByTestId("temperature-control")).toBeInTheDocument()
- expect(screen.getByTestId("rate-limit-seconds-control")).toBeInTheDocument()
- })
- it("hides all controls when fromWelcomeView is true", () => {
- renderApiOptions({ fromWelcomeView: true })
- // Check for absence of DiffSettingsControl text
- expect(screen.queryByText(/enable editing through diffs/i)).not.toBeInTheDocument()
- expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
- expect(screen.queryByTestId("rate-limit-seconds-control")).not.toBeInTheDocument()
- })
+ // Should show MaxOutputTokensControl
+ expect(screen.getByTestId("max-output-tokens-control")).toBeInTheDocument()
+ expect(screen.getByText("Max Output Tokens")).toBeInTheDocument()
- describe("thinking functionality", () => {
- it("should show ThinkingBudget for Anthropic models that support thinking", () => {
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "anthropic",
- apiModelId: "claude-3-7-sonnet-20250219:thinking",
- },
- })
-
- expect(screen.getByTestId("reasoning-budget")).toBeInTheDocument()
- })
-
- it("should show ThinkingBudget for Vertex models that support thinking", () => {
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "vertex",
- apiModelId: "claude-3-7-sonnet@20250219:thinking",
- },
- })
-
- expect(screen.getByTestId("reasoning-budget")).toBeInTheDocument()
- })
-
- it("should not show ThinkingBudget for models that don't support thinking", () => {
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "anthropic",
- apiModelId: "claude-3-opus-20240229",
- },
- })
-
- expect(screen.queryByTestId("reasoning-budget")).not.toBeInTheDocument()
- })
-
- // Note: We don't need to test the actual ThinkingBudget component functionality here
- // since we have separate tests for that component. We just need to verify that
- // it's included in the ApiOptions component when appropriate.
+ // Should NOT show ThinkingBudget
+ expect(screen.queryByTestId("reasoning-budget")).not.toBeInTheDocument()
+ expect(screen.queryByTestId("reasoning-effort")).not.toBeInTheDocument()
})
- describe("OpenAI provider tests", () => {
- it("removes reasoningEffort from openAiCustomModelInfo when unchecked", () => {
- const mockSetApiConfigurationField = jest.fn()
- const initialConfig = {
+ it("renders controls for thinking model", () => {
+ renderApiOptions({
+ apiConfiguration: {
apiProvider: "openai" as const,
- enableReasoningEffort: true,
- openAiCustomModelInfo: {
- ...openAiModelInfoSaneDefaults, // Start with defaults
- reasoningEffort: "low" as const, // Set an initial value
- },
- // Add other necessary default fields for openai provider if needed
- }
-
- renderApiOptions({
- apiConfiguration: initialConfig,
- setApiConfigurationField: mockSetApiConfigurationField,
- })
-
- // Find the checkbox by its test ID instead of label text
- // This is more reliable than using the label text which might be affected by translations
- const checkbox =
- screen.getByTestId("checkbox-input-settings:providers.setreasoninglevel") ||
- screen.getByTestId("checkbox-input-set-reasoning-level")
-
- // Simulate unchecking the checkbox
- fireEvent.click(checkbox)
-
- // 1. Check if enableReasoningEffort was set to false
- expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", false)
-
- // 2. Check if openAiCustomModelInfo was updated
- const updateCall = mockSetApiConfigurationField.mock.calls.find(
- (call) => call[0] === "openAiCustomModelInfo",
- )
- expect(updateCall).toBeDefined()
-
- // 3. Check if reasoningEffort property is absent in the updated info
- const updatedInfo = updateCall[1]
- expect(updatedInfo).not.toHaveProperty("reasoningEffort")
-
- // Optional: Check if other properties were preserved (example)
- expect(updatedInfo).toHaveProperty("contextWindow", openAiModelInfoSaneDefaults.contextWindow)
+ apiModelId: "thinking-model-with-max-tokens",
+ enableReasoningEffort: true, // Enable reasoning effort for thinking models
+ },
})
- it("does not render ReasoningEffort component when initially disabled", () => {
- const mockSetApiConfigurationField = jest.fn()
- const initialConfig = {
- apiProvider: "openai" as const,
- enableReasoningEffort: false, // Initially disabled
- openAiCustomModelInfo: {
- ...openAiModelInfoSaneDefaults,
- },
- }
-
- renderApiOptions({
- apiConfiguration: initialConfig,
- setApiConfigurationField: mockSetApiConfigurationField,
- })
-
- // Check that the ReasoningEffort select component is not rendered.
- expect(screen.queryByTestId("reasoning-effort")).not.toBeInTheDocument()
- })
+ // Should show both controls
+ expect(screen.getByTestId("max-output-tokens-control")).toBeInTheDocument()
+ expect(screen.getByTestId("reasoning-budget")).toBeInTheDocument()
+ })
- it("renders ReasoningEffort component and sets flag when checkbox is checked", () => {
- const mockSetApiConfigurationField = jest.fn()
- const initialConfig = {
+ it("renders no token controls for model without max tokens", () => {
+ renderApiOptions({
+ apiConfiguration: {
apiProvider: "openai" as const,
- enableReasoningEffort: false, // Initially disabled
- openAiCustomModelInfo: {
- ...openAiModelInfoSaneDefaults,
- },
- }
-
- renderApiOptions({
- apiConfiguration: initialConfig,
- setApiConfigurationField: mockSetApiConfigurationField,
- })
-
- const checkbox = screen.getByTestId("checkbox-input-settings:providers.setreasoninglevel")
-
- // Simulate checking the checkbox
- fireEvent.click(checkbox)
-
- // 1. Check if enableReasoningEffort was set to true
- expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", true)
-
- // We can't directly test the rendering of the ReasoningEffort component after the state change
- // without a more complex setup involving state management mocks or re-rendering.
- // However, we've tested the state update call.
+ apiModelId: "model-without-max-tokens",
+ },
})
- it.skip("updates reasoningEffort in openAiCustomModelInfo when select value changes", () => {
- const mockSetApiConfigurationField = jest.fn()
- const initialConfig = {
- apiProvider: "openai" as const,
- enableReasoningEffort: true, // Initially enabled
- openAiCustomModelInfo: {
- ...openAiModelInfoSaneDefaults,
- reasoningEffort: "low" as const,
- },
- }
+ // Should NOT show MaxOutputTokensControl
+ expect(screen.queryByTestId("max-output-tokens-control")).not.toBeInTheDocument()
- renderApiOptions({
- apiConfiguration: initialConfig,
- setApiConfigurationField: mockSetApiConfigurationField,
- })
-
- // Find the reasoning effort select among all comboboxes by its current value
- // const allSelects = screen.getAllByRole("combobox") as HTMLSelectElement[]
- // const reasoningSelect = allSelects.find(
- // (el) => el.value === initialConfig.openAiCustomModelInfo.reasoningEffort,
- // )
- // expect(reasoningSelect).toBeDefined()
- const selectContainer = screen.getByTestId("reasoning-effort")
- expect(selectContainer).toBeInTheDocument()
-
- console.log(selectContainer.querySelector("select")?.value)
-
- // Simulate changing the reasoning effort to 'high'
- fireEvent.change(selectContainer.querySelector("select")!, { target: { value: "high" } })
-
- // Check if setApiConfigurationField was called correctly for openAiCustomModelInfo
- expect(mockSetApiConfigurationField).toHaveBeenCalledWith(
- "openAiCustomModelInfo",
- expect.objectContaining({ reasoningEffort: "high" }),
- )
-
- // Check that other properties were preserved
- expect(mockSetApiConfigurationField).toHaveBeenCalledWith(
- "openAiCustomModelInfo",
- expect.objectContaining({
- contextWindow: openAiModelInfoSaneDefaults.contextWindow,
- }),
- )
- })
+ // Should NOT show ThinkingBudget
+ expect(screen.queryByTestId("reasoning-budget")).not.toBeInTheDocument()
})
- describe("LiteLLM provider tests", () => {
- it("renders LiteLLM component when provider is selected", () => {
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "litellm",
- litellmBaseUrl: "http://localhost:4000",
- litellmApiKey: "test-key",
- },
- })
-
- expect(screen.getByTestId("litellm-provider")).toBeInTheDocument()
- expect(screen.getByTestId("litellm-base-url")).toHaveValue("http://localhost:4000")
- expect(screen.getByTestId("litellm-api-key")).toHaveValue("test-key")
- })
-
- it("calls setApiConfigurationField when LiteLLM inputs change", () => {
- const mockSetApiConfigurationField = jest.fn()
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "litellm",
- },
- setApiConfigurationField: mockSetApiConfigurationField,
- })
+ it("shows temperature control", () => {
+ renderApiOptions()
- const baseUrlInput = screen.getByTestId("litellm-base-url")
- const apiKeyInput = screen.getByTestId("litellm-api-key")
+ const temperatureControl = screen.getByTestId("temperature-control")
+ expect(temperatureControl).toBeInTheDocument()
- fireEvent.change(baseUrlInput, { target: { value: "http://new-url:8000" } })
- fireEvent.change(apiKeyInput, { target: { value: "new-api-key" } })
+ const temperatureSlider = temperatureControl.querySelector("input")
+ expect(temperatureSlider).toBeInTheDocument()
+ expect(temperatureSlider!.type).toBe("range")
+ expect(temperatureSlider!.min).toBe("0")
+ expect(temperatureSlider!.max).toBe("2")
+ })
- expect(mockSetApiConfigurationField).toHaveBeenCalledWith("litellmBaseUrl", "http://new-url:8000")
- expect(mockSetApiConfigurationField).toHaveBeenCalledWith("litellmApiKey", "new-api-key")
+ it("displays provider-specific component", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "gemini" as const,
+ apiModelId: "gemini-2.5-pro-exp-03-25",
+ },
})
- it("shows refresh models button for LiteLLM", () => {
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "litellm",
- litellmBaseUrl: "http://localhost:4000",
- litellmApiKey: "test-key",
- },
- })
+ expect(screen.getByTestId("gemini-provider")).toBeInTheDocument()
+ })
- expect(screen.getByTestId("litellm-refresh-models")).toBeInTheDocument()
+ it("displays OpenAI Compatible component for openai provider", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "openai" as const,
+ apiModelId: "gpt-4",
+ },
})
- it("does not render LiteLLM component when other provider is selected", () => {
- renderApiOptions({
- apiConfiguration: {
- apiProvider: "anthropic",
- },
- })
-
- expect(screen.queryByTestId("litellm-provider")).not.toBeInTheDocument()
- })
+ expect(screen.getByTestId("openai-compatible-provider")).toBeInTheDocument()
})
})
diff --git a/webview-ui/src/components/settings/__tests__/MaxOutputTokensControl.test.tsx b/webview-ui/src/components/settings/__tests__/MaxOutputTokensControl.test.tsx
new file mode 100644
index 0000000000..122de43b6d
--- /dev/null
+++ b/webview-ui/src/components/settings/__tests__/MaxOutputTokensControl.test.tsx
@@ -0,0 +1,278 @@
+import React from "react"
+import { render, screen } from "@testing-library/react"
+
+import type { ProviderSettings, ModelInfo } from "@roo-code/types"
+
+import { MaxOutputTokensControl } from "../MaxOutputTokensControl"
+import { Slider } from "@src/components/ui"
+
+// Mock ResizeObserver globally
+global.ResizeObserver = jest.fn().mockImplementation(() => ({
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+}))
+// Static import of Slider removed again to avoid TS error for non-existent module
+
+// Mock i18n translation context
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+// Mock UI components
+jest.mock("@src/components/ui", () => ({
+ Slider: jest.fn((props) =>
),
+}))
+
+// Import the mocked Slider *after* jest.mock
+// We need to refer to it for assertions and clearing.
+const MockedSlider = jest.mocked(Slider)
+
+describe("MaxOutputTokensControl", () => {
+ const mockSetApiConfigurationField = jest.fn()
+ const mockApiConfiguration: ProviderSettings = {
+ apiProvider: "openai",
+ apiModelId: "test-model",
+ modelMaxTokens: undefined,
+ }
+
+ beforeEach(() => {
+ mockSetApiConfigurationField.mockClear()
+ MockedSlider.mockClear()
+ })
+
+ it("should render when modelInfo.maxTokens is a positive number", () => {
+ const modelInfo: Partial = {
+ maxTokens: 16000,
+ // Add other potentially accessed properties if needed by the component, even if undefined
+ }
+
+ render(
+ ,
+ )
+
+ expect(screen.getByTestId("max-output-tokens-control")).toBeInTheDocument()
+ })
+
+ it("should display the current value and label", () => {
+ const modelInfo: Partial = {
+ maxTokens: 16000,
+ }
+ const apiConfigurationWithOverride: ProviderSettings = {
+ ...mockApiConfiguration,
+ modelMaxTokens: 10000, // Override the default maxTokens
+ }
+
+ render(
+ ,
+ )
+
+ // Calculate the expected displayed value
+ const expectedValue = apiConfigurationWithOverride.modelMaxTokens ?? modelInfo.maxTokens!
+
+ // Assert that the current value is displayed
+ expect(screen.getByText(expectedValue.toString())).toBeInTheDocument()
+
+ // Assert that the label is displayed (using the mocked translation key)
+ expect(screen.getByText("settings:thinkingBudget.maxTokens")).toBeInTheDocument()
+ })
+
+ // Make the test async to use dynamic import
+ it("should pass min={2048} to the Slider component when rendered", async () => {
+ // Dynamically import Slider; it will be the mocked version due to jest.doMock
+ // Note: For this to work, the test function must be async and you await the import.
+ // However, for prop checking on a mock function, we can directly use the
+ // mockSliderImplementation defined in the jest.doMock scope.
+ // No need for dynamic import if we directly use the mock function instance.
+
+ const modelInfo: Partial = {
+ maxTokens: 16000, // A positive number to ensure rendering
+ }
+
+ render(
+ ,
+ )
+
+ // First, ensure the main control is rendered
+ expect(screen.getByTestId("max-output-tokens-control")).toBeInTheDocument()
+
+ // Assert that the mockSliderImplementation (which is what Slider becomes)
+ // was called with the correct 'min' prop.
+ // This test is expected to fail because Slider is not yet used or not with this prop.
+ expect(MockedSlider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ min: 2048,
+ }),
+ expect.anything(), // Context for React components
+ )
+ })
+
+ it("should pass max={modelInfo.maxTokens!} to the Slider component when rendered", () => {
+ const modelInfo: Partial = {
+ maxTokens: 32000, // A positive number to ensure rendering
+ }
+
+ render(
+ ,
+ )
+
+ // First, ensure the main control is rendered
+ expect(screen.getByTestId("max-output-tokens-control")).toBeInTheDocument()
+
+ // Assert that the MockedSlider was called with the correct 'max' prop.
+ expect(MockedSlider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ max: modelInfo.maxTokens,
+ }),
+ expect.anything(), // Context for React components
+ )
+ })
+
+ it("should pass step={1024} to the Slider component when rendered", () => {
+ const modelInfo: Partial = {
+ maxTokens: 16000, // A positive number to ensure rendering
+ }
+
+ render(
+ ,
+ )
+
+ // First, ensure the main control is rendered
+ expect(screen.getByTestId("max-output-tokens-control")).toBeInTheDocument()
+
+ // Assert that the MockedSlider was called with the correct 'step' prop.
+ expect(MockedSlider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ step: 1024,
+ }),
+ expect.anything(), // Context for React components
+ )
+ })
+
+ describe("should pass the correct initial value to the Slider component", () => {
+ it("when apiConfiguration.modelMaxTokens is defined", () => {
+ const modelInfo: Partial = {
+ maxTokens: 32000, // This should be ignored
+ }
+ const apiConfigurationWithOverride: ProviderSettings = {
+ ...mockApiConfiguration,
+ modelMaxTokens: 10000,
+ }
+
+ render(
+ ,
+ )
+
+ expect(MockedSlider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ value: [apiConfigurationWithOverride.modelMaxTokens],
+ }),
+ expect.anything(),
+ )
+ })
+
+ it("when apiConfiguration.modelMaxTokens is undefined", () => {
+ const modelInfo: Partial = {
+ maxTokens: 16000, // This should be used
+ }
+ const apiConfigurationWithoutOverride: ProviderSettings = {
+ ...mockApiConfiguration,
+ modelMaxTokens: undefined,
+ }
+
+ render(
+ ,
+ )
+
+ expect(MockedSlider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ value: [modelInfo.maxTokens!],
+ }),
+ expect.anything(),
+ )
+ })
+
+ it('should call setApiConfigurationField with "modelMaxTokens" and the new value when the slider value changes', () => {
+ const modelInfo: Partial = {
+ maxTokens: 32000, // A positive number to ensure rendering
+ }
+
+ render(
+ ,
+ )
+
+ // Simulate a value change by calling the onValueChange prop of the mocked Slider
+ // We need to access the props that the mocked component was called with.
+ // MockedSlider.mock.calls[0][0] gives us the props of the first render call.
+ const sliderProps = MockedSlider.mock.calls[0][0]
+ const newValue = [20000]
+ sliderProps.onValueChange?.(newValue)
+
+ // Assert that setApiConfigurationField was called with the correct arguments
+ expect(mockSetApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", newValue[0])
+ })
+ })
+
+ describe("should not render when modelInfo.maxTokens is not a positive number", () => {
+ const testCases = [
+ { name: "undefined", value: undefined },
+ { name: "null", value: null },
+ { name: "0", value: 0 },
+ ]
+
+ it.each(testCases)("when maxTokens is $name ($value)", ({ value }) => {
+ const modelInfo: Partial = {
+ maxTokens: value as any, // Use 'as any' to allow null/undefined for testing
+ }
+
+ render(
+ ,
+ )
+
+ // Check that the component renders null or an empty fragment
+ // by querying for the test ID and expecting it not to be there.
+ expect(screen.queryByTestId("max-output-tokens-control")).not.toBeInTheDocument()
+ // Alternatively, if the component renders null, its container might be empty or contain minimal structure.
+ // For a component that should render absolutely nothing, its direct container might be empty.
+ // However, queryByTestId is more robust for checking absence.
+ })
+ })
+})
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
index 60f2c29497..5773a87c14 100644
--- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -17,12 +17,31 @@ jest.mock("@/components/ui", () => ({
onChange={(e) => onValueChange([parseInt(e.target.value)])}
/>
),
+ Select: ({ children }: any) => {children}
,
+ SelectTrigger: ({ children }: any) => {children}
,
+ SelectValue: ({ placeholder }: any) => {placeholder}
,
+ SelectContent: ({ children }: any) => {children}
,
+ SelectItem: ({ children, value }: any) => {children}
,
+}))
+
+jest.mock("vscrui", () => ({
+ Checkbox: ({ children, checked, onChange }: any) => (
+
+ onChange(e.target.checked)}
+ />
+ {children}
+
+ ),
}))
describe("ThinkingBudget", () => {
const mockModelInfo: ModelInfo = {
supportsReasoningBudget: true,
- requiredReasoningBudget: true,
+ requiredReasoningBudget: false,
maxTokens: 16384,
contextWindow: 200000,
supportsPromptCache: true,
@@ -45,10 +64,6 @@ describe("ThinkingBudget", () => {
{...defaultProps}
modelInfo={{
...mockModelInfo,
- maxTokens: 16384,
- contextWindow: 200000,
- supportsPromptCache: true,
- supportsImages: true,
supportsReasoningBudget: false,
}}
/>,
@@ -57,10 +72,35 @@ describe("ThinkingBudget", () => {
expect(container.firstChild).toBeNull()
})
- it("should render sliders when model supports thinking", () => {
+ it("should render thinking budget slider when model supports reasoning budget", () => {
+ render( )
+
+ // Should only have thinking tokens slider, not max output tokens
+ expect(screen.getAllByTestId("slider")).toHaveLength(1)
+ expect(screen.getByText("settings:thinkingBudget.maxThinkingTokens")).toBeInTheDocument()
+ })
+
+ it("should show checkbox when reasoning is not required", () => {
render( )
- expect(screen.getAllByTestId("slider")).toHaveLength(2)
+ expect(screen.getByTestId("checkbox")).toBeInTheDocument()
+ expect(screen.getByText("settings:providers.useReasoning")).toBeInTheDocument()
+ })
+
+ it("should not show checkbox when reasoning is required", () => {
+ render(
+ ,
+ )
+
+ expect(screen.queryByTestId("checkbox")).not.toBeInTheDocument()
+ // Should show slider directly
+ expect(screen.getByTestId("slider")).toBeInTheDocument()
})
it("should update modelMaxThinkingTokens", () => {
@@ -69,24 +109,28 @@ describe("ThinkingBudget", () => {
render(
,
)
- const sliders = screen.getAllByTestId("slider")
- fireEvent.change(sliders[1], { target: { value: "5000" } })
+ const slider = screen.getByTestId("slider")
+ fireEvent.change(slider, { target: { value: "5000" } })
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
- it("should cap thinking tokens at 80% of max tokens", () => {
+ it("should cap thinking tokens at 80% of max output tokens", () => {
const setApiConfigurationField = jest.fn()
render(
,
)
@@ -96,34 +140,44 @@ describe("ThinkingBudget", () => {
})
it("should use default thinking tokens if not provided", () => {
- render( )
+ render( )
- // Default is 80% of max tokens, capped at 8192
- const sliders = screen.getAllByTestId("slider")
- expect(sliders[1]).toHaveValue("8000") // 80% of 10000
+ const slider = screen.getByTestId("slider")
+ // Default is 8192, but capped at 80% of max output tokens (16384 * 0.8 = 13107.2, rounded down)
+ // Since default max output tokens is 16384 and 8192 < 13107, it should be 8192
+ expect(slider).toHaveValue("8192")
})
- it("should use min thinking tokens of 1024", () => {
- render( )
+ it("should render reasoning effort select for models that support it", () => {
+ render(
+ ,
+ )
- const sliders = screen.getAllByTestId("slider")
- expect(sliders[1].getAttribute("min")).toBe("1024")
+ expect(screen.getByTestId("select")).toBeInTheDocument()
+ expect(screen.getByText("settings:providers.reasoningEffort.label")).toBeInTheDocument()
})
- it("should update max tokens when slider changes", () => {
+ it("should toggle enableReasoningEffort checkbox", () => {
const setApiConfigurationField = jest.fn()
render(
,
)
- const sliders = screen.getAllByTestId("slider")
- fireEvent.change(sliders[0], { target: { value: "12000" } })
+ const checkbox = screen.getByTestId("checkbox")
+ fireEvent.click(checkbox)
- expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", 12000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", true)
})
})