diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx index fb8f7c3d83..19a96af615 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx @@ -793,12 +793,30 @@ describe("SettingsModal", () => { }); describe("Global General", () => { - it("defaults persistAgentToolOutput checkbox to checked", async () => { + // Read-only default-render assertions are merged into one rendered + // instance to avoid re-rendering the full modal per pure-display check. + it("renders default global logging fields, helper text, and tracking repo control", async () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); + // persistAgentToolOutput defaults to checked; Star-on-GitHub control absent. expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).toBeChecked(); expect(screen.queryByRole("checkbox", { name: /Show "Star on GitHub" button in Settings header/i })).toBeNull(); + + // thinking-log checkboxes default to unchecked. + expect(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })).not.toBeChecked(); + expect(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })).not.toBeChecked(); + + // Helper descriptions render as small text (not .settings-field-help). + expect(document.querySelector(".settings-field-help")).toBeNull(); + const toolOutputHelper = screen.getByText(/When disabled, tool rows are still logged but detailed tool payloads are omitted/i); + expect(toolOutputHelper.closest("small")).toBeTruthy(); + const thinkingHelper = screen.getByText(/Leave both thinking toggles off to keep the original default behavior/i); + expect(thinkingHelper.closest("small")).toBeTruthy(); + + // Global default tracking repo control + inheritance hint render. + expect(screen.getByRole("combobox", { name: "Global default tracking repo" })).toBeInTheDocument(); + expect(screen.getByText(/Projects inherit this value when they do not set a project default tracking repo/i)).toBeInTheDocument(); }); it("reflects persisted unchecked value from global settings", async () => { @@ -817,14 +835,6 @@ describe("SettingsModal", () => { expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).not.toBeChecked(); }); - it("defaults thinking-log checkboxes to unchecked", async () => { - renderModal({ initialSection: "global-general" }); - await waitForSettingsModalReady(); - - expect(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })).not.toBeChecked(); - expect(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })).not.toBeChecked(); - }); - it("falls back to legacy thinking-log flag when granular fields are unset", async () => { mockFetchSettings.mockResolvedValue({ ...defaultSettings, @@ -885,28 +895,6 @@ describe("SettingsModal", () => { } }); - it("renders helper descriptions as small text for global logging fields", async () => { - renderModal({ initialSection: "global-general" }); - await waitForSettingsModalReady(); - - expect(document.querySelector(".settings-field-help")).toBeNull(); - - const toolOutputHelper = screen.getByText(/When disabled, tool rows are still logged but detailed tool payloads are omitted/i); - expect(toolOutputHelper.closest("small")).toBeTruthy(); - - const thinkingHelper = screen.getByText(/Leave both thinking toggles off to keep the original default behavior/i); - expect(thinkingHelper.closest("small")).toBeTruthy(); - }); - - it("renders global default tracking repo control", async () => { - renderModal({ initialSection: "global-general" }); - await waitForSettingsModalReady(); - - const control = screen.getByRole("combobox", { name: "Global default tracking repo" }) as HTMLSelectElement; - expect(control).toBeInTheDocument(); - expect(screen.getByText(/Projects inherit this value when they do not set a project default tracking repo/i)).toBeInTheDocument(); - }); - it("saves global default tracking repo via global settings payload only", async () => { mockFetchProjects.mockResolvedValueOnce([{ id: "p-1", name: "Alpha" }]); mockFetchGitRemotes.mockResolvedValueOnce([{ name: "origin", owner: "octo", repo: "global-default", url: "https://github.com/octo/global-default.git" }]); @@ -1528,7 +1516,7 @@ describe("SettingsModal", () => { }); describe("settings header actions", () => { - it("renders Help, Discord, and GitHub star controls", async () => { + it("renders Help, Discord (hardened), and GitHub star controls", async () => { renderModal(); await waitForSettingsModalReady(); @@ -1548,30 +1536,29 @@ describe("SettingsModal", () => { expect(helpLink).toHaveAttribute("target", "_blank"); expect(helpLink).toHaveAttribute("rel", expect.stringContaining("noopener")); expect(helpLink).toHaveAttribute("rel", expect.stringContaining("noreferrer")); - }); - - it("renders Discord link with hardened external attributes", async () => { - renderModal(); - await waitForSettingsModalReady(); + // Discord link uses hardened external attributes and branded icon. const discordLink = screen.getByRole("link", { name: "Join our Discord" }); expect(discordLink).toHaveAttribute("href", "https://discord.gg/ksrfuy7WYR"); expect(discordLink).toHaveAttribute("target", "_blank"); expect(discordLink).toHaveAttribute("rel", expect.stringContaining("noopener")); expect(discordLink).toHaveAttribute("rel", expect.stringContaining("noreferrer")); - expect(within(discordLink).getByTestId("discord-icon")).toBeInTheDocument(); expect(within(discordLink).queryByTestId("lucide-message-circle")).not.toBeInTheDocument(); }); }); describe("settings version display", () => { - it("renders the app version from the health endpoint", async () => { + it("renders the app version and the check-for-updates button in the header", async () => { renderModal(); await waitForSettingsModalReady(); expect(await screen.findByText("Version 1.2.3")).toBeInTheDocument(); expect(mockFetchDashboardHealth).toHaveBeenCalledTimes(1); + + expect(screen.getByRole("button", { name: "Check for updates" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Check Now" })).not.toBeInTheDocument(); + expect(screen.queryByText("Manually check for the latest version right now.")).not.toBeInTheDocument(); }); it("keeps settings interactive when version lookup fails", async () => { @@ -1587,15 +1574,6 @@ describe("SettingsModal", () => { expect(addToast).not.toHaveBeenCalled(); }); - it("renders check for updates button in header", async () => { - renderModal(); - await waitForSettingsModalReady(); - - expect(screen.getByRole("button", { name: "Check for updates" })).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Check Now" })).not.toBeInTheDocument(); - expect(screen.queryByText("Manually check for the latest version right now.")).not.toBeInTheDocument(); - }); - it("clicking check for updates shows up-to-date message", async () => { mockCheckForUpdates.mockResolvedValueOnce({ currentVersion: "1.2.3", @@ -1762,65 +1740,13 @@ describe("SettingsModal", () => { expect(mockExportSettings).toHaveBeenCalled(); }); - // Assert the filename uses fusion-settings- prefix + // Assert the filename uses fusion-settings- prefix and NOT the legacy kb- prefix. expect(createdElements.length).toBeGreaterThanOrEqual(1); const anchorElement = createdElements[0]; expect(anchorElement.download).toMatch(/^fusion-settings-/); expect(anchorElement.download).toMatch(/^fusion-settings-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.json$/); - }); - - it("does not use kb-settings- prefix for exported filename", async () => { - const mockExportData: SettingsExportData = { - version: 1, - exportedAt: "2026-04-04T12:00:00.000Z", - global: undefined, - project: { maxConcurrent: 2 }, - }; - mockExportSettings.mockResolvedValue(mockExportData); - - // Capture filenames set on dynamically-created anchor elements - const capturedFilenames: string[] = []; - const originalCreateElement = document.createElement.bind(document); - vi.spyOn(document, "createElement").mockImplementation((tagName: string) => { - const el = originalCreateElement(tagName); - if (tagName.toLowerCase() === "a") { - const origDownloadDescriptor = Object.getOwnPropertyDescriptor( - HTMLAnchorElement.prototype, - "download" - ); - Object.defineProperty(el, "download", { - set(v: string) { - capturedFilenames.push(v); - origDownloadDescriptor?.set?.call(el, v); - }, - get() { - return origDownloadDescriptor?.get?.call(el) ?? ""; - }, - configurable: true, - }); - } - return el; - }); - - vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:http://localhost/mock"); - vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {}); - - renderModal(); - - await waitFor(() => { - expect(mockFetchSettings).toHaveBeenCalled(); - }); - - fireEvent.click(screen.getByTitle("Export settings to JSON file")); - - await waitFor(() => { - expect(mockExportSettings).toHaveBeenCalled(); - }); - - // Negative assertion: filename must NOT use the old kb- prefix - expect(capturedFilenames.length).toBeGreaterThanOrEqual(1); - for (const filename of capturedFilenames) { - expect(filename).not.toMatch(/^kb-settings-/); + for (const { download } of createdElements) { + expect(download).not.toMatch(/^kb-settings-/); } }); }); @@ -3391,35 +3317,29 @@ describe("SettingsModal", () => { expect(await screen.findByText("Experimental Features")).toBeInTheDocument(); }); - it("shows known experimental features (Insights, Roadmaps) even when no custom features are configured", async () => { + // Read-only feature-list assertions share one render + section open. + // All pure label-presence checks are asserted against a single rendered + // instance to avoid re-rendering the full modal per feature. + it("shows known features and the full experimental feature list with a single Dev Server toggle", async () => { renderModal(); - await openExperimentalFeaturesSection(); - // Known features should always be shown + // Known features are always shown even with no custom features configured. expect(screen.getByText("Insights")).toBeInTheDocument(); expect(screen.getByText("Roadmaps")).toBeInTheDocument(); - }); - it.each([ - "Research View", - "Evals View", - "Chat Rooms", - "Sandbox (command isolation)", - "Planning-style Agent Onboarding", - ])("shows %s in the Experimental Features list", async (featureLabel) => { - renderModal(); - await openExperimentalFeaturesSection(); - expect(screen.getByLabelText(featureLabel)).toBeInTheDocument(); - }); + for (const featureLabel of [ + "Research View", + "Evals View", + "Chat Rooms", + "Sandbox (command isolation)", + "Planning-style Agent Onboarding", + ]) { + expect(screen.getByLabelText(featureLabel)).toBeInTheDocument(); + } - it("shows a single canonical Dev Server toggle", async () => { - renderModal(); - - await openExperimentalFeaturesSection(); - - const devServerToggles = screen.getAllByLabelText("Dev Server"); - expect(devServerToggles).toHaveLength(1); + // Dev Server has a single canonical toggle (no legacy duplicate). + expect(screen.getAllByLabelText("Dev Server")).toHaveLength(1); }); it("does not render duplicate Dev Server rows when legacy and canonical keys are both present", async () => {