From d45a8191d54c95939689e9b1641075983160af39 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 2 Apr 2026 09:19:51 -0700 Subject: [PATCH] test(FN-673): update SettingsModal tests for section split --- .../__tests__/SettingsModal.test.tsx | 186 ++++++++++++++---- 1 file changed, 150 insertions(+), 36 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx index 374dc9abf..29c570327 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx @@ -370,7 +370,7 @@ describe("SettingsModal", () => { expect(payload.pollIntervalMs).toBe(15000); }); - it("saving project settings does not update global settings endpoint", async () => { + it("saving in General section updates project settings with task prefix", async () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); @@ -381,30 +381,68 @@ describe("SettingsModal", () => { fireEvent.click(screen.getByText("Save")); await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); - // Global settings should NOT be called when in a project section - expect(updateGlobalSettings).not.toHaveBeenCalled(); + // Verify taskPrefix is saved as project setting + const projectPayload = (updateSettings as ReturnType).mock.calls[0][0]; + expect(projectPayload.taskPrefix).toBe("TEST"); }); - it("saving in Model section only updates global settings", async () => { + it("saving in Default Model section updates global settings with default model", async () => { + const user = userEvent.setup(); render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Save")); - // Model section is global-scoped - await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1)); + // Select a model + const trigger = screen.getByLabelText("Default Model"); + await user.click(trigger); + await user.click(screen.getByText("Claude Sonnet 4.5")); - // Project settings should NOT be updated when in a global section - expect(updateSettings).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText("Save")); + + // Verify default model settings are saved as global settings + await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1)); + const globalPayload = (updateGlobalSettings as ReturnType).mock.calls[0][0]; + expect(globalPayload.defaultProvider).toBe("anthropic"); + expect(globalPayload.defaultModelId).toBe("claude-sonnet-4-5"); }); - it("shows Model in sidebar", async () => { + it("saving in Execution Model section updates project settings with planning and validator models", async () => { + const user = userEvent.setup(); render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - expect(screen.getAllByText("Model").length).toBeGreaterThanOrEqual(1); + fireEvent.click(screen.getByText("Execution Model")); + await waitFor(() => expect(fetchModels).toHaveBeenCalled()); + + // Select planning model + const planningTrigger = screen.getByLabelText("Planning Model"); + await user.click(planningTrigger); + await user.click(screen.getByText("Claude Sonnet 4.5")); + + // Select validator model + const validatorTrigger = screen.getByLabelText("Validator Model"); + await user.click(validatorTrigger); + await user.click(screen.getByText("GPT-4o")); + + fireEvent.click(screen.getByText("Save")); + + // Verify planning and validator settings are saved as project settings + await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); + const projectPayload = (updateSettings as ReturnType).mock.calls[0][0]; + expect(projectPayload.planningProvider).toBe("anthropic"); + expect(projectPayload.planningModelId).toBe("claude-sonnet-4-5"); + expect(projectPayload.validatorProvider).toBe("openai"); + expect(projectPayload.validatorModelId).toBe("gpt-4o"); + }); + + it("shows Default Model and Execution Model in sidebar", async () => { + render(); + await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); + + expect(screen.getAllByText("Default Model").length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText("Execution Model").length).toBeGreaterThanOrEqual(1); }); it("supports creating and saving a model preset", async () => { @@ -461,7 +499,7 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Dropdown trigger should be present @@ -486,7 +524,7 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Open dropdown and select a model @@ -514,7 +552,7 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Open dropdown and select "Use default" @@ -545,7 +583,77 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); + await waitFor(() => expect(fetchModels).toHaveBeenCalled()); + + expect(screen.getByText("No models available. Configure authentication first.")).toBeTruthy(); + }); + + // --- Execution Model section tests (project-scoped) --- + + it("shows Execution Model section with planning and validator model dropdowns", async () => { + render(); + await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); + + fireEvent.click(screen.getByText("Execution Model")); + await waitFor(() => expect(fetchModels).toHaveBeenCalled()); + + // Both dropdowns should be present + expect(screen.getByLabelText("Planning Model")).toBeTruthy(); + expect(screen.getByLabelText("Validator Model")).toBeTruthy(); + }); + + it("selecting a planning model updates form with provider and model ID", async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); + + fireEvent.click(screen.getByText("Execution Model")); + await waitFor(() => expect(fetchModels).toHaveBeenCalled()); + + // Open planning model dropdown and select a model + const trigger = screen.getByLabelText("Planning Model"); + await user.click(trigger); + await user.click(screen.getByText("Claude Sonnet 4.5")); + + fireEvent.click(screen.getByText("Save")); + // planningProvider and planningModelId are project settings, so they go through updateSettings + await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); + + const payload = (updateSettings as ReturnType).mock.calls[0][0]; + expect(payload.planningProvider).toBe("anthropic"); + expect(payload.planningModelId).toBe("claude-sonnet-4-5"); + }); + + it("selecting a validator model updates form with provider and model ID", async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); + + fireEvent.click(screen.getByText("Execution Model")); + await waitFor(() => expect(fetchModels).toHaveBeenCalled()); + + // Open validator model dropdown and select a model + const trigger = screen.getByLabelText("Validator Model"); + await user.click(trigger); + await user.click(screen.getByText("GPT-4o")); + + fireEvent.click(screen.getByText("Save")); + // validatorProvider and validatorModelId are project settings, so they go through updateSettings + await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); + + const payload = (updateSettings as ReturnType).mock.calls[0][0]; + expect(payload.validatorProvider).toBe("openai"); + expect(payload.validatorModelId).toBe("gpt-4o"); + }); + + it("shows empty state in Execution Model section when no models available", async () => { + (fetchModels as ReturnType).mockResolvedValueOnce([]); + + render(); + await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); + + fireEvent.click(screen.getByText("Execution Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); expect(screen.getByText("No models available. Configure authentication first.")).toBeTruthy(); @@ -660,7 +768,7 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // CustomModelDropdown renders as a button trigger, not a select element @@ -692,11 +800,11 @@ describe("SettingsModal", () => { expect(elementsWithStyle.length).toBe(1); }); - it("shows Thinking Effort dropdown with correct options in Model section", async () => { + it("shows Thinking Effort dropdown with correct options in Default Model section", async () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); const select = screen.getByLabelText("Thinking Effort") as HTMLSelectElement; @@ -710,7 +818,7 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); const select = screen.getByLabelText("Thinking Effort") as HTMLSelectElement; @@ -734,7 +842,7 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); expect(screen.queryByLabelText("Thinking Effort")).toBeNull(); @@ -833,20 +941,21 @@ describe("SettingsModal", () => { expect(layout!.querySelector(".settings-content")).toBeTruthy(); }); - it("has .settings-sidebar with 12 .settings-nav-item buttons for all sections", async () => { + it("has .settings-sidebar with 13 .settings-nav-item buttons for all sections", async () => { const { container } = render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); const sidebar = container.querySelector(".settings-sidebar"); expect(sidebar).toBeTruthy(); const navItems = sidebar!.querySelectorAll(".settings-nav-item"); - expect(navItems.length).toBe(12); + expect(navItems.length).toBe(13); // Labels include scope emoji indicators (🌐 for global, 📁 for project) const labels = Array.from(navItems).map((el) => el.textContent); expect(labels).toEqual([ "📁General", - "🌐Model", + "🌐Default Model", + "📁Execution Model", "📁Model Presets", "📁AI Summarization", "🌐Appearance", @@ -1111,12 +1220,12 @@ describe("SettingsModal", () => { }); // Model filter tests with CustomModelDropdown - it("renders filter input in Model section dropdown", async () => { + it("renders filter input in Default Model section dropdown", async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Open dropdown to access filter input @@ -1127,12 +1236,12 @@ describe("SettingsModal", () => { expect(screen.getByPlaceholderText("Filter models…")).toBeTruthy(); }); - it("filters default model options in Model section", async () => { + it("filters default model options in Default Model section", async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Open dropdown @@ -1151,12 +1260,12 @@ describe("SettingsModal", () => { expect(screen.queryByText("GPT-4o")).not.toBeInTheDocument(); }); - it("clear button resets filter in Model section", async () => { + it("clear button resets filter in Default Model section", async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Open dropdown @@ -1183,12 +1292,12 @@ describe("SettingsModal", () => { expect(screen.getByText("Claude Sonnet 4.5")).toBeTruthy(); }); - it("shows empty state in Model section when filter matches nothing", async () => { + it("shows empty state in Default Model section when filter matches nothing", async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Model")); + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); // Open dropdown @@ -1402,10 +1511,15 @@ describe("SettingsModal", () => { expect(container.querySelector(".settings-scope-project")).toBeTruthy(); expect(container.querySelector(".settings-scope-global")).toBeNull(); - // Switch to Model → should show global banner - fireEvent.click(screen.getByText("Model")); + // Switch to Default Model → should show global banner + fireEvent.click(screen.getByText("Default Model")); expect(container.querySelector(".settings-scope-global")).toBeTruthy(); expect(container.querySelector(".settings-scope-project")).toBeNull(); + + // Switch to Execution Model → should show project banner + fireEvent.click(screen.getByText("Execution Model")); + expect(container.querySelector(".settings-scope-project")).toBeTruthy(); + expect(container.querySelector(".settings-scope-global")).toBeNull(); }); // --- Settings save error handling tests --- @@ -1429,8 +1543,8 @@ describe("SettingsModal", () => { render(); await waitFor(() => expect(fetchSettings).toHaveBeenCalled()); - // Switch to Model section (global scope) - fireEvent.click(screen.getByText("Model")); + // Switch to Default Model section (global scope) + fireEvent.click(screen.getByText("Default Model")); await waitFor(() => expect(fetchModels).toHaveBeenCalled()); fireEvent.click(screen.getByText("Save"));