import React from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import SettingsService from "#/api/settings-service/settings-service.api"; import { SdkSectionPage } from "#/components/features/settings/sdk-settings/sdk-section-page"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; import { Settings } from "#/types/settings"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; const mockUseSearchParams = vi.fn(); vi.mock("react-router", async () => { const actual = await vi.importActual("react-router"); return { ...actual, useSearchParams: () => mockUseSearchParams(), useRevalidator: () => ({ revalidate: vi.fn() }), }; }); const mockUseConfig = vi.fn(); vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => mockUseConfig(), })); function buildSettings(overrides: Partial = {}): Settings { return { ...MOCK_DEFAULT_USER_SETTINGS, ...overrides, agent_settings: { ...MOCK_DEFAULT_USER_SETTINGS.agent_settings, ...overrides.agent_settings, }, agent_settings_schema: overrides.agent_settings_schema ?? MOCK_DEFAULT_USER_SETTINGS.agent_settings_schema, }; } function buildSavableSettings(): Settings { return buildSettings({ agent_settings_schema: { model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { key: "llm.endpoint", label: "Endpoint", section: "llm", section_label: "LLM", value_type: "string", default: "https://api.example.com", choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, ], }, ], }, agent_settings: { "llm.endpoint": "https://api.example.com", }, }); } function renderSdkSectionPage( props: React.ComponentProps, ) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); mockUseConfig.mockReturnValue({ data: { app_mode: "oss" }, isLoading: false, }); mockUseSearchParams.mockReturnValue([{ get: () => null }, vi.fn()]); return render(React.createElement(SdkSectionPage, props), { wrapper: ({ children }) => ( {children} ), }); } beforeEach(() => { vi.restoreAllMocks(); mockUseConfig.mockReturnValue({ data: { app_mode: "oss" }, isLoading: false, }); mockUseSearchParams.mockReturnValue([{ get: () => null }, vi.fn()]); }); describe("SdkSectionPage", () => { it("renders advanced-only fields when a custom initial view is provided", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { key: "llm.model", label: "Model", section: "llm", section_label: "LLM", value_type: "string", default: "openai/gpt-4o", choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "llm.api_version", label: "API Version", section: "llm", section_label: "LLM", value_type: "string", default: null, choices: [], depends_on: [], prominence: "major", secret: false, required: false, }, ], }, ], }; vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings({ agent_settings_schema: schema, agent_settings: { "llm.model": "openai/gpt-4o", }, }), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], getInitialView: () => "advanced", }); expect( await screen.findByTestId("sdk-settings-llm.api_version"), ).toBeInTheDocument(); }); it("preserves the selected view when parent rerenders with the same settings", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { key: "llm.model", label: "Model", section: "llm", section_label: "LLM", value_type: "string", default: "wren/claude-opus-4-5-20251101", choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "llm.base_url", label: "Base URL", section: "llm", section_label: "LLM", value_type: "string", default: null, choices: [], depends_on: [], prominence: "major", secret: false, required: false, }, ], }, ], }; vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings({ agent_settings_schema: schema, agent_settings: { "llm.model": "wren/claude-opus-4-5-20251101", }, }), ); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); function Wrapper() { const [externalValue, setExternalValue] = React.useState(""); return ( ( setExternalValue(event.target.value)} /> )} /> ); } render(, { wrapper: ({ children }) => ( {children} ), }); await screen.findByTestId("sdk-section-advanced-toggle"); await userEvent.click(screen.getByTestId("sdk-section-advanced-toggle")); await screen.findByTestId("sdk-settings-llm.base_url"); await userEvent.type(screen.getByTestId("external-state-input"), "a"); await waitFor(() => { expect( screen.getByTestId("sdk-settings-llm.base_url"), ).toBeInTheDocument(); }); }); it("resets from advanced to the inferred basic view after saving when advanced settings match defaults", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { key: "llm.endpoint", label: "Endpoint", section: "llm", section_label: "LLM", value_type: "string", default: "https://api.example.com", choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "llm.api_version", label: "API Version", section: "llm", section_label: "LLM", value_type: "string", default: null, choices: [], depends_on: [], prominence: "major", secret: false, required: false, }, ], }, ], }; let persistedSettings = buildSettings({ agent_settings_schema: schema, agent_settings: { llm: { endpoint: "https://api.example.com", }, }, }); const getSettingsSpy = vi .spyOn(SettingsService, "getSettings") .mockImplementation(async () => structuredClone(persistedSettings)); vi.spyOn(SettingsService, "saveSettings").mockImplementation( async (payload) => { const agentSettings = payload.agent_settings_diff as Record< string, unknown >; const llmSettings = (agentSettings.llm ?? {}) as Record< string, unknown >; persistedSettings = buildSettings({ agent_settings_schema: schema, agent_settings: { llm: { endpoint: typeof llmSettings.endpoint === "string" ? llmSettings.endpoint : "https://api.example.com", }, }, }); return true; }, ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], }); await screen.findByTestId("sdk-section-advanced-toggle"); await userEvent.click(screen.getByTestId("sdk-section-advanced-toggle")); await screen.findByTestId("sdk-settings-llm.api_version"); const endpointInput = await screen.findByTestId( "sdk-settings-llm.endpoint", ); await userEvent.clear(endpointInput); await userEvent.type(endpointInput, "https://api.changed.example.com"); await userEvent.click(screen.getByTestId("save-button")); await waitFor(() => { expect(getSettingsSpy).toHaveBeenCalledTimes(2); }); await waitFor(() => { expect( screen.queryByTestId("sdk-settings-llm.api_version"), ).not.toBeInTheDocument(); }); }); it("resets from all to the inferred basic view after saving when detailed settings match defaults", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { key: "llm.endpoint", label: "Endpoint", section: "llm", section_label: "LLM", value_type: "string", default: "https://api.example.com", choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "llm.timeout", label: "Timeout", section: "llm", section_label: "LLM", value_type: "integer", default: 30, choices: [], depends_on: [], prominence: "minor", secret: false, required: false, }, ], }, ], }; let persistedSettings = buildSettings({ agent_settings_schema: schema, agent_settings: { llm: { endpoint: "https://api.example.com", }, }, }); const getSettingsSpy = vi .spyOn(SettingsService, "getSettings") .mockImplementation(async () => structuredClone(persistedSettings)); vi.spyOn(SettingsService, "saveSettings").mockImplementation( async (payload) => { const agentSettings = payload.agent_settings_diff as Record< string, unknown >; const llmSettings = (agentSettings.llm ?? {}) as Record< string, unknown >; persistedSettings = buildSettings({ agent_settings_schema: schema, agent_settings: { llm: { endpoint: typeof llmSettings.endpoint === "string" ? llmSettings.endpoint : "https://api.example.com", }, }, }); return true; }, ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], }); await screen.findByTestId("sdk-section-all-toggle"); await userEvent.click(screen.getByTestId("sdk-section-all-toggle")); await screen.findByTestId("sdk-settings-llm.timeout"); const endpointInput = await screen.findByTestId( "sdk-settings-llm.endpoint", ); await userEvent.clear(endpointInput); await userEvent.type(endpointInput, "https://api.changed.example.com"); await userEvent.click(screen.getByTestId("save-button")); await waitFor(() => { expect(getSettingsSpy).toHaveBeenCalledTimes(2); }); await waitFor(() => { expect( screen.queryByTestId("sdk-settings-llm.timeout"), ).not.toBeInTheDocument(); }); }); it("shows the advanced toggle when it is forced for a critical-only schema", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSavableSettings(), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], forceShowAdvancedView: true, }); await screen.findByTestId("sdk-section-basic-toggle"); expect( screen.getByTestId("sdk-section-advanced-toggle"), ).toBeInTheDocument(); expect( screen.queryByTestId("sdk-section-all-toggle"), ).not.toBeInTheDocument(); }); it("shows the all toggle instead of an empty advanced tier for minor-only schemas", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "condenser", label: "Condenser", fields: [ { key: "condenser.enabled", label: "Enable memory condensation", section: "condenser", section_label: "Condenser", value_type: "boolean", default: true, choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "condenser.max_size", label: "Max size", section: "condenser", section_label: "Condenser", value_type: "integer", default: 240, choices: [], depends_on: ["condenser.enabled"], prominence: "minor", secret: false, required: true, }, ], }, ], }; vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings({ agent_settings_schema: schema, agent_settings: { "condenser.enabled": true, "condenser.max_size": 240, }, }), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["condenser"] }, ], }); await screen.findByTestId("sdk-section-basic-toggle"); expect( screen.queryByTestId("sdk-section-advanced-toggle"), ).not.toBeInTheDocument(); expect(screen.getByTestId("sdk-section-all-toggle")).toBeInTheDocument(); }); it("renders URL-like schema fields as url inputs", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "verification", label: "Verification", fields: [ { key: "verification.critic_enabled", label: "Enable critic", section: "verification", section_label: "Verification", value_type: "boolean", default: true, choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "verification.critic_server_url", label: "Critic server URL", section: "verification", section_label: "Verification", value_type: "string", default: null, choices: [], depends_on: ["verification.critic_enabled"], prominence: "minor", secret: false, required: false, }, ], }, ], }; vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings({ agent_settings_schema: schema, agent_settings: { verification: { critic_enabled: true, critic_server_url: "https://critic.example.com", }, }, }), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["verification"] }, ], getInitialView: () => "all", }); expect( await screen.findByTestId("sdk-settings-verification.critic_server_url"), ).toHaveAttribute("type", "url"); }); it("shows iterative refinement in the basic view once critic is enabled", async () => { const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "verification", label: "Verification", fields: [ { key: "verification.critic_enabled", label: "Enable critic", section: "verification", section_label: "Verification", value_type: "boolean", default: true, choices: [], depends_on: [], prominence: "critical", secret: false, required: true, }, { key: "verification.enable_iterative_refinement", label: "Enable iterative refinement", section: "verification", section_label: "Verification", value_type: "boolean", default: false, choices: [], depends_on: ["verification.critic_enabled"], prominence: "critical", secret: false, required: false, }, ], }, ], }; vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings({ agent_settings_schema: schema, agent_settings: { verification: { critic_enabled: false, enable_iterative_refinement: false, }, }, }), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["verification"] }, ], }); expect( await screen.findByTestId("sdk-settings-verification.critic_enabled"), ).toBeInTheDocument(); expect( screen.queryByTestId( "sdk-settings-verification.enable_iterative_refinement", ), ).not.toBeInTheDocument(); await userEvent.click( screen.getByTestId("sdk-settings-verification.critic_enabled"), ); expect( await screen.findByTestId( "sdk-settings-verification.enable_iterative_refinement", ), ).toBeInTheDocument(); }); it("shows a success toast after saving settings", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSavableSettings(), ); vi.spyOn(SettingsService, "saveSettings").mockResolvedValue(true); const displaySuccessToastSpy = vi.spyOn( ToastHandlers, "displaySuccessToast", ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], }); const endpointInput = await screen.findByTestId( "sdk-settings-llm.endpoint", ); await userEvent.clear(endpointInput); await userEvent.type(endpointInput, "https://api.changed.example.com"); await userEvent.click(screen.getByTestId("save-button")); await waitFor(() => { expect(displaySuccessToastSpy).toHaveBeenCalled(); }); }); it("shows an error toast when saving settings fails", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSavableSettings(), ); vi.spyOn(SettingsService, "saveSettings").mockRejectedValue( new AxiosError("Request failed"), ); const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast"); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], }); const endpointInput = await screen.findByTestId( "sdk-settings-llm.endpoint", ); await userEvent.clear(endpointInput); await userEvent.type(endpointInput, "https://api.changed.example.com"); await userEvent.click(screen.getByTestId("save-button")); await waitFor(() => { expect(displayErrorToastSpy).toHaveBeenCalled(); }); }); it("renders trailingActions after the view toggle buttons", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSavableSettings(), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], trailingActions: ( ), }); // The slot has no view toggles (buildSavableSettings schema has only a // critical field), so this also locks the behavior that trailingActions // alone is enough to force the strip to render. expect(await screen.findByTestId("trailing-action")).toBeInTheDocument(); }); it("drops duplicate union sections that don't match the source variant", async () => { // SDK agent-settings is a discriminated union: the "llm" key exists under // both the "wren" and "acp" variants (#14892). A variant-targeted // source must render only its variant's section, not the duplicate. const llmField = (key: string, label: string) => ({ key, label, description: null, section: "llm", section_label: "LLM", value_type: "string" as const, default: null, choices: [], depends_on: [], prominence: "critical" as const, secret: false, required: false, }); const schema: NonNullable = { model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", variant: "wren", fields: [ llmField("llm.model", "Model"), llmField("llm.auth_type", "Authentication"), ], }, { key: "llm", label: "LLM", variant: "acp", fields: [ llmField("llm.model", "Model"), llmField("llm.acp_only", "ACP Only"), ], }, ], }; vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings({ agent_settings_schema: schema }), ); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"], variant: "wren", }, ], }); expect( await screen.findByTestId("sdk-settings-llm.auth_type"), ).toBeInTheDocument(); // wren fields render exactly once; the acp duplicate is dropped. expect(screen.getAllByTestId("sdk-settings-llm.model")).toHaveLength(1); expect( screen.queryByTestId("sdk-settings-llm.acp_only"), ).not.toBeInTheDocument(); }); it("allows saving custom payloads when only external state is dirty", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue(buildSettings()); const saveSettingsSpy = vi .spyOn(SettingsService, "saveSettings") .mockResolvedValue(true); renderSdkSectionPage({ settingsSources: [ { settingsSource: "agent_settings", sectionKeys: ["llm"] }, ], extraDirty: true, buildPayload: (payload) => ({ ...payload, search_api_key: "external-search-key", }), }); await userEvent.click(await screen.findByTestId("save-button")); await waitFor(() => { expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ search_api_key: "external-search-key" }), ); }); }); });