diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx index 3942cc8fc1..68e44d73e9 100644 --- a/frontend/__tests__/routes/llm-settings.test.tsx +++ b/frontend/__tests__/routes/llm-settings.test.tsx @@ -910,6 +910,162 @@ describe("Form submission", () => { }); }); +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 () => { diff --git a/frontend/__tests__/utils/has-advanced-settings-set.test.ts b/frontend/__tests__/utils/has-advanced-settings-set.test.ts index 36c7a7b609..be928262d1 100644 --- a/frontend/__tests__/utils/has-advanced-settings-set.test.ts +++ b/frontend/__tests__/utils/has-advanced-settings-set.test.ts @@ -29,5 +29,75 @@ describe("hasAdvancedSettingsSet", () => { }), ).toBe(true); }); + + test("enable_default_condenser is disabled", () => { + // Arrange + const settings = { + ...DEFAULT_SETTINGS, + enable_default_condenser: false, + }; + + // Act + const result = hasAdvancedSettingsSet(settings); + + // Assert + expect(result).toBe(true); + }); + + test("condenser_max_size is customized above default", () => { + // Arrange + const settings = { + ...DEFAULT_SETTINGS, + condenser_max_size: 200, + }; + + // Act + const result = hasAdvancedSettingsSet(settings); + + // Assert + expect(result).toBe(true); + }); + + test("condenser_max_size is customized below default", () => { + // Arrange + const settings = { + ...DEFAULT_SETTINGS, + condenser_max_size: 50, + }; + + // Act + const result = hasAdvancedSettingsSet(settings); + + // Assert + expect(result).toBe(true); + }); + + test("search_api_key is set to non-empty value", () => { + // Arrange + const settings = { + ...DEFAULT_SETTINGS, + search_api_key: "test-api-key-123", + }; + + // Act + const result = hasAdvancedSettingsSet(settings); + + // Assert + expect(result).toBe(true); + }); + + test("search_api_key with whitespace is treated as set", () => { + // Arrange + const settings = { + ...DEFAULT_SETTINGS, + search_api_key: " test-key ", + }; + + // Act + const result = hasAdvancedSettingsSet(settings); + + // Assert + expect(result).toBe(true); + }); }); }); diff --git a/frontend/src/utils/has-advanced-settings-set.ts b/frontend/src/utils/has-advanced-settings-set.ts index b873425239..8e3de2be9c 100644 --- a/frontend/src/utils/has-advanced-settings-set.ts +++ b/frontend/src/utils/has-advanced-settings-set.ts @@ -1,6 +1,48 @@ import { DEFAULT_SETTINGS } from "#/services/settings"; import { Settings } from "#/types/settings"; -export const hasAdvancedSettingsSet = (settings: Partial): boolean => - Object.keys(settings).length > 0 && - (!!settings.llm_base_url || settings.agent !== DEFAULT_SETTINGS.agent); +/** + * Determines if any advanced-only settings are configured. + * Advanced-only settings are those that appear only in the Advanced Settings view + * and not in the Basic Settings view. + * + * Advanced-only fields: + * - llm_base_url: Custom base URL for LLM API + * - agent: Custom agent selection (when not using default) + * - enable_default_condenser: Memory condenser toggle (when disabled, as default is enabled) + * - condenser_max_size: Custom condenser size (when different from default) + * - search_api_key: Search API key (when set) + */ +export const hasAdvancedSettingsSet = ( + settings: Partial, +): boolean => { + if (Object.keys(settings).length === 0) { + return false; + } + + // Check for advanced-only settings that differ from defaults + const hasBaseUrl = + !!settings.llm_base_url && settings.llm_base_url.trim() !== ""; + const hasCustomAgent = + settings.agent !== undefined && settings.agent !== DEFAULT_SETTINGS.agent; + // Default is true, so only check if explicitly disabled + const hasDisabledCondenser = settings.enable_default_condenser === false; + // Check if condenser size differs from default (default is 120) + const hasCustomCondenserSize = + settings.condenser_max_size !== undefined && + settings.condenser_max_size !== null && + settings.condenser_max_size !== DEFAULT_SETTINGS.condenser_max_size; + // Check if search API key is set (non-empty string) + const hasSearchApiKey = + settings.search_api_key !== undefined && + settings.search_api_key !== null && + settings.search_api_key.trim() !== ""; + + return ( + hasBaseUrl || + hasCustomAgent || + hasDisabledCondenser || + hasCustomCondenserSize || + hasSearchApiKey + ); +};