import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import LlmSettingsScreen from "#/routes/llm-settings"; import SettingsService from "#/api/settings-service/settings-service.api"; import { MOCK_DEFAULT_USER_SETTINGS, resetTestHandlersMockSettings, } from "#/mocks/handlers"; import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; import OptionService from "#/api/option-service/option-service.api"; // Mock react-router hooks const mockUseSearchParams = vi.fn(); vi.mock("react-router", () => ({ useSearchParams: () => mockUseSearchParams(), })); // Mock useIsAuthed hook const mockUseIsAuthed = vi.fn(); vi.mock("#/hooks/query/use-is-authed", () => ({ useIsAuthed: () => mockUseIsAuthed(), })); const renderLlmSettingsScreen = () => render(, { wrapper: ({ children }) => ( {children} ), }); beforeEach(() => { vi.resetAllMocks(); resetTestHandlersMockSettings(); // Default mock for useSearchParams - returns empty params mockUseSearchParams.mockReturnValue([ { get: () => null, }, vi.fn(), ]); // Default mock for useIsAuthed - returns authenticated by default mockUseIsAuthed.mockReturnValue({ data: true, isLoading: false }); }); describe("Content", () => { describe("Basic form", () => { it("should render the basic form by default", async () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicFom = screen.getByTestId("llm-settings-form-basic"); within(basicFom).getByTestId("llm-provider-input"); within(basicFom).getByTestId("llm-model-input"); within(basicFom).getByTestId("llm-api-key-input"); within(basicFom).getByTestId("llm-api-key-help-anchor"); }); it("should render the default values if non exist", async () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const provider = screen.getByTestId("llm-provider-input"); const model = screen.getByTestId("llm-model-input"); const apiKey = screen.getByTestId("llm-api-key-input"); await waitFor(() => { expect(provider).toHaveValue("OpenHands"); expect(model).toHaveValue("claude-opus-4-5-20251101"); expect(apiKey).toHaveValue(""); expect(apiKey).toHaveProperty("placeholder", ""); }); }); it("should render the existing settings values", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, llm_model: "openai/gpt-4o", llm_api_key_set: true, }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const provider = screen.getByTestId("llm-provider-input"); const model = screen.getByTestId("llm-model-input"); const apiKey = screen.getByTestId("llm-api-key-input"); await waitFor(() => { expect(provider).toHaveValue("OpenAI"); expect(model).toHaveValue("gpt-4o"); expect(apiKey).toHaveValue(""); expect(apiKey).toHaveProperty("placeholder", ""); expect(screen.getByTestId("set-indicator")).toBeInTheDocument(); }); }); }); describe("Advanced form", () => { it("should conditionally show security analyzer based on confirmation mode", async () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); // Enable advanced mode first const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); const confirmation = screen.getByTestId( "enable-confirmation-mode-switch", ); // Initially confirmation mode is false, so security analyzer should not be visible expect(confirmation).not.toBeChecked(); expect( screen.queryByTestId("security-analyzer-input"), ).not.toBeInTheDocument(); // Enable confirmation mode await userEvent.click(confirmation); expect(confirmation).toBeChecked(); // Security analyzer should now be visible screen.getByTestId("security-analyzer-input"); // Disable confirmation mode again await userEvent.click(confirmation); expect(confirmation).not.toBeChecked(); // Security analyzer should be hidden again expect( screen.queryByTestId("security-analyzer-input"), ).not.toBeInTheDocument(); }); it("should render the advanced form if the switch is toggled", async () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); const basicForm = screen.getByTestId("llm-settings-form-basic"); expect( screen.queryByTestId("llm-settings-form-advanced"), ).not.toBeInTheDocument(); expect(basicForm).toBeInTheDocument(); await userEvent.click(advancedSwitch); expect( screen.queryByTestId("llm-settings-form-advanced"), ).toBeInTheDocument(); expect(basicForm).not.toBeInTheDocument(); const advancedForm = screen.getByTestId("llm-settings-form-advanced"); within(advancedForm).getByTestId("llm-custom-model-input"); within(advancedForm).getByTestId("base-url-input"); within(advancedForm).getByTestId("llm-api-key-input"); within(advancedForm).getByTestId("llm-api-key-help-anchor-advanced"); within(advancedForm).getByTestId("agent-input"); within(advancedForm).getByTestId("enable-memory-condenser-switch"); await userEvent.click(advancedSwitch); expect( screen.queryByTestId("llm-settings-form-advanced"), ).not.toBeInTheDocument(); expect(screen.getByTestId("llm-settings-form-basic")).toBeInTheDocument(); }); it("should render the default advanced settings", async () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); expect(advancedSwitch).not.toBeChecked(); await userEvent.click(advancedSwitch); const model = screen.getByTestId("llm-custom-model-input"); const baseUrl = screen.getByTestId("base-url-input"); const apiKey = screen.getByTestId("llm-api-key-input"); const agent = screen.getByTestId("agent-input"); const condensor = screen.getByTestId("enable-memory-condenser-switch"); expect(model).toHaveValue("openhands/claude-opus-4-5-20251101"); expect(baseUrl).toHaveValue(""); expect(apiKey).toHaveValue(""); expect(apiKey).toHaveProperty("placeholder", ""); expect(agent).toHaveValue("CodeActAgent"); expect(condensor).toBeChecked(); }); it("should render the advanced form if existings settings are advanced", async () => { const hasAdvancedSettingsSetSpy = vi.spyOn( AdvancedSettingsUtlls, "hasAdvancedSettingsSet", ); hasAdvancedSettingsSetSpy.mockReturnValue(true); renderLlmSettingsScreen(); await waitFor(() => { const advancedSwitch = screen.getByTestId("advanced-settings-switch"); expect(advancedSwitch).toBeChecked(); screen.getByTestId("llm-settings-form-advanced"); }); }); it("should render existing advanced settings correctly", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, llm_model: "openai/gpt-4o", llm_base_url: "https://api.openai.com/v1/chat/completions", llm_api_key_set: true, agent: "CoActAgent", confirmation_mode: true, enable_default_condenser: false, security_analyzer: "none", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const model = screen.getByTestId("llm-custom-model-input"); const baseUrl = screen.getByTestId("base-url-input"); const apiKey = screen.getByTestId("llm-api-key-input"); const agent = screen.getByTestId("agent-input"); const confirmation = screen.getByTestId( "enable-confirmation-mode-switch", ); const condensor = screen.getByTestId("enable-memory-condenser-switch"); const securityAnalyzer = screen.getByTestId("security-analyzer-input"); await waitFor(() => { expect(model).toHaveValue("openai/gpt-4o"); expect(baseUrl).toHaveValue( "https://api.openai.com/v1/chat/completions", ); expect(apiKey).toHaveValue(""); expect(apiKey).toHaveProperty("placeholder", ""); expect(agent).toHaveValue("CoActAgent"); expect(confirmation).toBeChecked(); expect(condensor).not.toBeChecked(); expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE"); }); }); it("should omit invariant and custom analyzers when V1 is enabled", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, confirmation_mode: true, security_analyzer: "llm", v1_enabled: true, }); const getSecurityAnalyzersSpy = vi.spyOn( OptionService, "getSecurityAnalyzers", ); getSecurityAnalyzersSpy.mockResolvedValue([ "llm", "none", "invariant", "custom", ]); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); const securityAnalyzer = await screen.findByTestId( "security-analyzer-input", ); await userEvent.click(securityAnalyzer); // Only llm + none should be available when V1 is enabled screen.getByText("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT"); screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE"); expect( screen.queryByText("SETTINGS$SECURITY_ANALYZER_INVARIANT"), ).not.toBeInTheDocument(); expect(screen.queryByText("custom")).not.toBeInTheDocument(); }); it("should include invariant analyzer option when V1 is disabled", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, confirmation_mode: true, security_analyzer: "llm", v1_enabled: false, }); const getSecurityAnalyzersSpy = vi.spyOn( OptionService, "getSecurityAnalyzers", ); getSecurityAnalyzersSpy.mockResolvedValue(["llm", "none", "invariant"]); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); const securityAnalyzer = await screen.findByTestId( "security-analyzer-input", ); await userEvent.click(securityAnalyzer); expect( screen.getByText("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT"), ).toBeInTheDocument(); expect( screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE"), ).toBeInTheDocument(); expect( screen.getByText("SETTINGS$SECURITY_ANALYZER_INVARIANT"), ).toBeInTheDocument(); }); }); it.todo("should render an indicator if the llm api key is set"); describe("API key visibility in Basic Settings", () => { it("should hide API key input when SaaS mode is enabled and OpenHands provider is selected", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return APP_MODE for these tests getConfigSpy.mockResolvedValue({ APP_MODE: "saas", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicForm = screen.getByTestId("llm-settings-form-basic"); const provider = within(basicForm).getByTestId("llm-provider-input"); // Verify OpenHands is selected by default await waitFor(() => { expect(provider).toHaveValue("OpenHands"); }); // API key input should not be visible when OpenHands provider is selected in SaaS mode expect( within(basicForm).queryByTestId("llm-api-key-input"), ).not.toBeInTheDocument(); expect( within(basicForm).queryByTestId("llm-api-key-help-anchor"), ).not.toBeInTheDocument(); }); it("should show API key input when SaaS mode is enabled and non-OpenHands provider is selected", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return APP_MODE for these tests getConfigSpy.mockResolvedValue({ APP_MODE: "saas", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicForm = screen.getByTestId("llm-settings-form-basic"); const provider = within(basicForm).getByTestId("llm-provider-input"); // Select OpenAI provider await userEvent.click(provider); const providerOption = screen.getByText("OpenAI"); await userEvent.click(providerOption); await waitFor(() => { expect(provider).toHaveValue("OpenAI"); }); // API key input should be visible when non-OpenHands provider is selected in SaaS mode expect( within(basicForm).getByTestId("llm-api-key-input"), ).toBeInTheDocument(); expect( within(basicForm).getByTestId("llm-api-key-help-anchor"), ).toBeInTheDocument(); }); it("should show API key input when OSS mode is enabled and OpenHands provider is selected", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return APP_MODE for these tests getConfigSpy.mockResolvedValue({ APP_MODE: "oss", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicForm = screen.getByTestId("llm-settings-form-basic"); const provider = within(basicForm).getByTestId("llm-provider-input"); // Verify OpenHands is selected by default await waitFor(() => { expect(provider).toHaveValue("OpenHands"); }); // API key input should be visible when OSS mode is enabled (even with OpenHands provider) expect( within(basicForm).getByTestId("llm-api-key-input"), ).toBeInTheDocument(); expect( within(basicForm).getByTestId("llm-api-key-help-anchor"), ).toBeInTheDocument(); }); it("should show API key input when OSS mode is enabled and non-OpenHands provider is selected", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return APP_MODE for these tests getConfigSpy.mockResolvedValue({ APP_MODE: "oss", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicForm = screen.getByTestId("llm-settings-form-basic"); const provider = within(basicForm).getByTestId("llm-provider-input"); // Select OpenAI provider await userEvent.click(provider); const providerOption = screen.getByText("OpenAI"); await userEvent.click(providerOption); await waitFor(() => { expect(provider).toHaveValue("OpenAI"); }); // API key input should be visible when OSS mode is enabled expect( within(basicForm).getByTestId("llm-api-key-input"), ).toBeInTheDocument(); expect( within(basicForm).getByTestId("llm-api-key-help-anchor"), ).toBeInTheDocument(); }); it("should hide API key input when switching from non-OpenHands to OpenHands provider in SaaS mode", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return APP_MODE for these tests getConfigSpy.mockResolvedValue({ APP_MODE: "saas", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicForm = screen.getByTestId("llm-settings-form-basic"); const provider = within(basicForm).getByTestId("llm-provider-input"); // Start with OpenAI provider await userEvent.click(provider); const openAIOption = screen.getByText("OpenAI"); await userEvent.click(openAIOption); await waitFor(() => { expect(provider).toHaveValue("OpenAI"); }); // API key input should be visible with OpenAI expect( within(basicForm).getByTestId("llm-api-key-input"), ).toBeInTheDocument(); // Switch to OpenHands provider await userEvent.click(provider); const openHandsOption = screen.getByText("OpenHands"); await userEvent.click(openHandsOption); await waitFor(() => { expect(provider).toHaveValue("OpenHands"); }); // API key input should now be hidden expect( within(basicForm).queryByTestId("llm-api-key-input"), ).not.toBeInTheDocument(); expect( within(basicForm).queryByTestId("llm-api-key-help-anchor"), ).not.toBeInTheDocument(); }); it("should show API key input when switching from OpenHands to non-OpenHands provider in SaaS mode", async () => { const getConfigSpy = vi.spyOn(OptionService, "getConfig"); // @ts-expect-error - only return APP_MODE for these tests getConfigSpy.mockResolvedValue({ APP_MODE: "saas", }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const basicForm = screen.getByTestId("llm-settings-form-basic"); const provider = within(basicForm).getByTestId("llm-provider-input"); // Verify OpenHands is selected by default await waitFor(() => { expect(provider).toHaveValue("OpenHands"); }); // API key input should be hidden with OpenHands expect( within(basicForm).queryByTestId("llm-api-key-input"), ).not.toBeInTheDocument(); // Switch to OpenAI provider await userEvent.click(provider); const openAIOption = screen.getByText("OpenAI"); await userEvent.click(openAIOption); await waitFor(() => { expect(provider).toHaveValue("OpenAI"); }); // API key input should now be visible expect( within(basicForm).getByTestId("llm-api-key-input"), ).toBeInTheDocument(); expect( within(basicForm).getByTestId("llm-api-key-help-anchor"), ).toBeInTheDocument(); }); }); }); describe("Form submission", () => { it("should submit the basic form with the correct values", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const provider = screen.getByTestId("llm-provider-input"); const model = screen.getByTestId("llm-model-input"); const apiKey = screen.getByTestId("llm-api-key-input"); // select provider await userEvent.click(provider); const providerOption = screen.getByText("OpenAI"); await userEvent.click(providerOption); expect(provider).toHaveValue("OpenAI"); // enter api key await userEvent.type(apiKey, "test-api-key"); // select model await userEvent.click(model); const modelOption = screen.getByText("gpt-4o"); await userEvent.click(modelOption); expect(model).toHaveValue("gpt-4o"); const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ llm_model: "openai/gpt-4o", llm_api_key: "test-api-key", }), ); }); it("should submit the advanced form with the correct values", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); const model = screen.getByTestId("llm-custom-model-input"); const baseUrl = screen.getByTestId("base-url-input"); const apiKey = screen.getByTestId("llm-api-key-input"); const agent = screen.getByTestId("agent-input"); const confirmation = screen.getByTestId("enable-confirmation-mode-switch"); const condensor = screen.getByTestId("enable-memory-condenser-switch"); // enter custom model await userEvent.clear(model); await userEvent.type(model, "openai/gpt-4o"); expect(model).toHaveValue("openai/gpt-4o"); // enter base url await userEvent.type(baseUrl, "https://api.openai.com/v1/chat/completions"); expect(baseUrl).toHaveValue("https://api.openai.com/v1/chat/completions"); // enter api key await userEvent.type(apiKey, "test-api-key"); // toggle confirmation mode await userEvent.click(confirmation); expect(confirmation).toBeChecked(); // toggle memory condensor await userEvent.click(condensor); expect(condensor).not.toBeChecked(); // select agent await userEvent.click(agent); const agentOption = screen.getByText("CoActAgent"); await userEvent.click(agentOption); expect(agent).toHaveValue("CoActAgent"); // select security analyzer const securityAnalyzer = screen.getByTestId("security-analyzer-input"); await userEvent.click(securityAnalyzer); const securityAnalyzerOption = screen.getByText( "SETTINGS$SECURITY_ANALYZER_NONE", ); await userEvent.click(securityAnalyzerOption); const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ llm_model: "openai/gpt-4o", llm_base_url: "https://api.openai.com/v1/chat/completions", agent: "CoActAgent", confirmation_mode: true, enable_default_condenser: false, security_analyzer: null, }), ); }); it("should disable the button if there are no changes in the basic form", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, llm_model: "openai/gpt-4o", llm_api_key_set: true, }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); screen.getByTestId("llm-settings-form-basic"); const submitButton = screen.getByTestId("submit-button"); expect(submitButton).toBeDisabled(); const model = screen.getByTestId("llm-model-input"); const apiKey = screen.getByTestId("llm-api-key-input"); // select model await userEvent.click(model); const modelOption = screen.getByText("gpt-4o-mini"); await userEvent.click(modelOption); expect(model).toHaveValue("gpt-4o-mini"); expect(submitButton).not.toBeDisabled(); // reset model await userEvent.click(model); const modelOption2 = screen.getByText("gpt-4o"); await userEvent.click(modelOption2); expect(model).toHaveValue("gpt-4o"); expect(submitButton).toBeDisabled(); // set api key await userEvent.type(apiKey, "test-api-key"); expect(apiKey).toHaveValue("test-api-key"); expect(submitButton).not.toBeDisabled(); // reset api key await userEvent.clear(apiKey); expect(apiKey).toHaveValue(""); expect(submitButton).toBeDisabled(); }); it("should disable the button if there are no changes in the advanced form", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, llm_model: "openai/gpt-4o", llm_base_url: "https://api.openai.com/v1/chat/completions", llm_api_key_set: true, confirmation_mode: true, }); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); await screen.findByTestId("llm-settings-form-advanced"); const submitButton = await screen.findByTestId("submit-button"); expect(submitButton).toBeDisabled(); const model = await screen.findByTestId("llm-custom-model-input"); const baseUrl = await screen.findByTestId("base-url-input"); const apiKey = await screen.findByTestId("llm-api-key-input"); const agent = await screen.findByTestId("agent-input"); const condensor = await screen.findByTestId( "enable-memory-condenser-switch", ); // Confirmation mode switch is now in basic settings, always visible const confirmation = await screen.findByTestId( "enable-confirmation-mode-switch", ); // enter custom model await userEvent.type(model, "-mini"); expect(model).toHaveValue("openai/gpt-4o-mini"); expect(submitButton).not.toBeDisabled(); // reset model await userEvent.clear(model); expect(model).toHaveValue(""); expect(submitButton).toBeDisabled(); await userEvent.type(model, "openai/gpt-4o"); expect(model).toHaveValue("openai/gpt-4o"); expect(submitButton).toBeDisabled(); // enter base url await userEvent.type(baseUrl, "/extra"); expect(baseUrl).toHaveValue( "https://api.openai.com/v1/chat/completions/extra", ); expect(submitButton).not.toBeDisabled(); await userEvent.clear(baseUrl); expect(baseUrl).toHaveValue(""); expect(submitButton).not.toBeDisabled(); await userEvent.type(baseUrl, "https://api.openai.com/v1/chat/completions"); expect(baseUrl).toHaveValue("https://api.openai.com/v1/chat/completions"); expect(submitButton).toBeDisabled(); // set api key await userEvent.type(apiKey, "test-api-key"); expect(apiKey).toHaveValue("test-api-key"); expect(submitButton).not.toBeDisabled(); // reset api key await userEvent.clear(apiKey); expect(apiKey).toHaveValue(""); expect(submitButton).toBeDisabled(); // set agent await userEvent.clear(agent); await userEvent.type(agent, "test-agent"); expect(agent).toHaveValue("test-agent"); expect(submitButton).not.toBeDisabled(); // reset agent await userEvent.clear(agent); expect(agent).toHaveValue(""); expect(submitButton).toBeDisabled(); await userEvent.type(agent, "CodeActAgent"); expect(agent).toHaveValue("CodeActAgent"); expect(submitButton).toBeDisabled(); // toggle confirmation mode await userEvent.click(confirmation); expect(confirmation).not.toBeChecked(); expect(submitButton).not.toBeDisabled(); await userEvent.click(confirmation); expect(confirmation).toBeChecked(); expect(submitButton).toBeDisabled(); // toggle memory condensor await userEvent.click(condensor); expect(condensor).not.toBeChecked(); expect(submitButton).not.toBeDisabled(); await userEvent.click(condensor); expect(condensor).toBeChecked(); expect(submitButton).toBeDisabled(); // select security analyzer const securityAnalyzer = await screen.findByTestId( "security-analyzer-input", ); await userEvent.click(securityAnalyzer); const securityAnalyzerOption = screen.getByText( "SETTINGS$SECURITY_ANALYZER_NONE", ); await userEvent.click(securityAnalyzerOption); expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE"); expect(submitButton).not.toBeDisabled(); // revert back to original value await userEvent.click(securityAnalyzer); const originalSecurityAnalyzerOption = screen.getByText( "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT", ); await userEvent.click(originalSecurityAnalyzerOption); expect(securityAnalyzer).toHaveValue( "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT", ); expect(submitButton).toBeDisabled(); }); it("should reset button state when switching between forms", async () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); const submitButton = screen.getByTestId("submit-button"); expect(submitButton).toBeDisabled(); // dirty the basic form const apiKey = screen.getByTestId("llm-api-key-input"); await userEvent.type(apiKey, "test-api-key"); expect(submitButton).not.toBeDisabled(); await userEvent.click(advancedSwitch); expect(submitButton).toBeDisabled(); // dirty the advanced form const model = screen.getByTestId("llm-custom-model-input"); await userEvent.type(model, "openai/gpt-4o"); expect(submitButton).not.toBeDisabled(); await userEvent.click(advancedSwitch); expect(submitButton).toBeDisabled(); }); // flaky test it.skip("should disable the button when submitting changes", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const apiKey = screen.getByTestId("llm-api-key-input"); await userEvent.type(apiKey, "test-api-key"); const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ llm_api_key: "test-api-key", }), ); expect(submitButton).toHaveTextContent("Saving..."); expect(submitButton).toBeDisabled(); await waitFor(() => { expect(submitButton).toHaveTextContent("Save"); expect(submitButton).toBeDisabled(); }); }); it("should clear advanced settings when saving basic settings", async () => { const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, llm_model: "openai/gpt-4o", llm_base_url: "https://api.openai.com/v1/chat/completions", llm_api_key_set: true, confirmation_mode: true, }); const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); // Component automatically shows advanced view when advanced settings exist // Switch to basic view to test clearing advanced settings const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); // Now we should be in basic view await screen.findByTestId("llm-settings-form-basic"); const provider = screen.getByTestId("llm-provider-input"); const model = screen.getByTestId("llm-model-input"); // select provider await userEvent.click(provider); const providerOption = screen.getByText("OpenHands"); await userEvent.click(providerOption); // select model await userEvent.click(model); const modelOption = screen.getByText("claude-sonnet-4-20250514"); await userEvent.click(modelOption); const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ llm_model: "openhands/claude-sonnet-4-20250514", llm_base_url: "", confirmation_mode: false, // Confirmation mode is now an advanced setting, should be cleared when saving basic settings }), ); }); }); describe("View persistence after saving advanced settings", () => { it("should remain on Advanced view after saving when memory condenser is disabled", async () => { // Arrange: Start with default settings (basic view) const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, }); const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); saveSettingsSpy.mockResolvedValue(true); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); // Verify we start in basic view expect(screen.getByTestId("llm-settings-form-basic")).toBeInTheDocument(); // Act: User manually switches to Advanced view const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); await screen.findByTestId("llm-settings-form-advanced"); // User disables memory condenser (advanced-only setting) const condenserSwitch = screen.getByTestId( "enable-memory-condenser-switch", ); expect(condenserSwitch).toBeChecked(); await userEvent.click(condenserSwitch); expect(condenserSwitch).not.toBeChecked(); // Mock the updated settings that will be returned after save getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, enable_default_condenser: false, // Now disabled }); // User saves settings const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); // Assert: View should remain on Advanced after save await waitFor(() => { expect( screen.getByTestId("llm-settings-form-advanced"), ).toBeInTheDocument(); expect( screen.queryByTestId("llm-settings-form-basic"), ).not.toBeInTheDocument(); expect(advancedSwitch).toBeChecked(); }); }); it("should remain on Advanced view after saving when condenser max size is customized", async () => { // Arrange: Start with default settings const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, }); const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); saveSettingsSpy.mockResolvedValue(true); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); // Act: User manually switches to Advanced view const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); await screen.findByTestId("llm-settings-form-advanced"); // User sets custom condenser max size (advanced-only setting) const condenserMaxSizeInput = screen.getByTestId( "condenser-max-size-input", ); await userEvent.clear(condenserMaxSizeInput); await userEvent.type(condenserMaxSizeInput, "200"); // Mock the updated settings that will be returned after save getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, condenser_max_size: 200, // Custom value }); // User saves settings const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); // Assert: View should remain on Advanced after save await waitFor(() => { expect( screen.getByTestId("llm-settings-form-advanced"), ).toBeInTheDocument(); expect( screen.queryByTestId("llm-settings-form-basic"), ).not.toBeInTheDocument(); expect(advancedSwitch).toBeChecked(); }); }); it("should remain on Advanced view after saving when search API key is set", async () => { // Arrange: Start with default settings (non-SaaS mode to show search API key field) const getConfigSpy = vi.spyOn(OptionService, "getConfig"); getConfigSpy.mockResolvedValue({ APP_MODE: "oss", GITHUB_CLIENT_ID: "fake-github-client-id", POSTHOG_CLIENT_KEY: "fake-posthog-client-key", FEATURE_FLAGS: { ENABLE_BILLING: false, HIDE_LLM_SETTINGS: false, ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, }, }); const getSettingsSpy = vi.spyOn(SettingsService, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, search_api_key: "", // Default empty value }); const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); saveSettingsSpy.mockResolvedValue(true); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); // Act: User manually switches to Advanced view const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); await screen.findByTestId("llm-settings-form-advanced"); // User sets search API key (advanced-only setting) const searchApiKeyInput = screen.getByTestId("search-api-key-input"); await userEvent.type(searchApiKeyInput, "test-search-api-key"); // Mock the updated settings that will be returned after save getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, search_api_key: "test-search-api-key", // Now set }); // User saves settings const submitButton = screen.getByTestId("submit-button"); await userEvent.click(submitButton); // Assert: View should remain on Advanced after save await waitFor(() => { expect( screen.getByTestId("llm-settings-form-advanced"), ).toBeInTheDocument(); expect( screen.queryByTestId("llm-settings-form-basic"), ).not.toBeInTheDocument(); expect(advancedSwitch).toBeChecked(); }); }); }); describe("Status toasts", () => { describe("Basic form", () => { it("should call displaySuccessToast when the settings are saved", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); const displaySuccessToastSpy = vi.spyOn( ToastHandlers, "displaySuccessToast", ); renderLlmSettingsScreen(); // Toggle setting to change const apiKeyInput = await screen.findByTestId("llm-api-key-input"); await userEvent.type(apiKeyInput, "test-api-key"); const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled()); }); it("should call displayErrorToast when the settings fail to save", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast"); saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings")); renderLlmSettingsScreen(); // Toggle setting to change const apiKeyInput = await screen.findByTestId("llm-api-key-input"); await userEvent.type(apiKeyInput, "test-api-key"); const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); expect(displayErrorToastSpy).toHaveBeenCalled(); }); }); describe("Advanced form", () => { it("should call displaySuccessToast when the settings are saved", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); const displaySuccessToastSpy = vi.spyOn( ToastHandlers, "displaySuccessToast", ); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); await screen.findByTestId("llm-settings-form-advanced"); // Toggle setting to change const apiKeyInput = await screen.findByTestId("llm-api-key-input"); await userEvent.type(apiKeyInput, "test-api-key"); const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled()); }); it("should call displayErrorToast when the settings fail to save", async () => { const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings"); const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast"); saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings")); renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); const advancedSwitch = screen.getByTestId("advanced-settings-switch"); await userEvent.click(advancedSwitch); await screen.findByTestId("llm-settings-form-advanced"); // Toggle setting to change const apiKeyInput = await screen.findByTestId("llm-api-key-input"); await userEvent.type(apiKeyInput, "test-api-key"); const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); expect(displayErrorToastSpy).toHaveBeenCalled(); }); }); });