import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor, fireEvent, act, cleanup } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { loadAllAppCss } from "../../test/cssFixture"; import type { AgentHeartbeatRun } from "../../api"; import type { AgentLogEntry } from "@fusion/core"; import { DEFAULT_HEARTBEAT_INTERVAL_MS } from "../../utils/heartbeatIntervals"; import { MOCK_SKILLS, createMockAgent, mockConfirm, mockDeleteAgent, mockFetchAgent, mockFetchAgentBudgetStatus, mockFetchAgentChildren, mockFetchAgentLogsWithMeta, mockFetchAgentMailbox, mockFetchAgentMemoryFile, mockFetchAgentMemoryFiles, mockFetchAgentRunDetail, mockFetchAgentRunLogs, mockFetchAgentRuns, mockFetchAgentTasks, mockFetchAgents, mockFetchChainOfCommand, mockFetchCompanies, mockFetchDiscoveredSkills, mockFetchModels, mockFetchPluginRuntimes, mockFetchSkillContent, mockFetchWorkspaceFileContent, mockMarkMessageRead, mockResetAgentBudget, mockSaveAgentMemoryFile, mockSaveWorkspaceFileContent, mockStartAgentRun, mockSubscribeSse, mockUpdateAgent, mockUpdateAgentInstructions, mockUpdateAgentMemory, mockUpdateAgentSoul, mockUpdateAgentState, mockUpdateGlobalSettings, mockUpgradeAgentHeartbeatProcedure, setupAgentDetailMocks, } from "./AgentDetailView.test-helpers"; import { AgentDetailView } from "../AgentDetailView"; describe("AgentDetailView — advanced settings", () => { beforeEach(() => { setupAgentDetailMocks(); }); describe("Advanced Settings", () => { const navigateToSettings = async (user: ReturnType) => { await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); await user.click(screen.getByText("Settings")); }; it("opens AI Interview in edit mode and applies draft values to local settings fields", async () => { const user = userEvent.setup(); render( , ); await navigateToSettings(user); await user.click(await screen.findByRole("button", { name: "AI Interview" })); expect(await screen.findByTestId("mock-ai-interview-modal")).toBeInTheDocument(); expect(screen.getByTestId("mock-ai-interview-mode")).toHaveTextContent("edit"); expect(screen.getByTestId("mock-ai-existing-config").textContent).toContain("Test Agent"); await user.click(screen.getByRole("button", { name: "Apply Draft" })); await waitFor(() => { expect((screen.getByLabelText("Name") as HTMLInputElement).value).toBe("Interviewed Agent"); expect((screen.getByLabelText("Title") as HTMLInputElement).value).toBe("Draft Title"); expect((screen.getByLabelText("Icon") as HTMLInputElement).value).toBe("🧠"); expect((screen.getByLabelText("Role") as HTMLSelectElement).value).toBe("reviewer"); }); expect(mockUpdateAgent).not.toHaveBeenCalled(); await user.click(screen.getByRole("button", { name: "Save Settings" })); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ name: "Interviewed Agent", role: "reviewer", title: "Draft Title", reportsTo: "agent-002", runtimeConfig: expect.objectContaining({ model: "openai/gpt-4o" }), metadata: { skills: ["skill-1"] }, }), undefined, ); }); }); it("shows settings delete control for idle and paused agents", async () => { const user = userEvent.setup(); mockFetchAgent.mockResolvedValue(createMockAgent({ state: "idle" })); const idleRender = render( , ); await navigateToSettings(user); expect(await screen.findByRole("button", { name: "Delete Agent" })).toBeEnabled(); idleRender.unmount(); mockFetchAgent.mockResolvedValue(createMockAgent({ state: "paused" })); render( , ); await navigateToSettings(user); expect(await screen.findByRole("button", { name: "Delete Agent" })).toBeEnabled(); }); it("deletes an agent from Settings after confirmation", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ state: "idle" })); const addToast = vi.fn(); const onClose = vi.fn(); const onMutationSuccess = vi.fn(); const user = userEvent.setup(); render( , ); await navigateToSettings(user); await user.click(await screen.findByRole("button", { name: "Delete Agent" })); await waitFor(() => { expect(mockConfirm).toHaveBeenCalledWith({ title: "Delete Agent", message: 'Delete agent "Test Agent"? This cannot be undone.', danger: true, }); expect(mockDeleteAgent).toHaveBeenCalledWith("agent-001", "proj_123"); expect(addToast).toHaveBeenCalledWith('Agent "Test Agent" deleted', "success"); expect(onMutationSuccess).toHaveBeenCalledWith({ agentId: "agent-001", deleted: true }); expect(onClose).toHaveBeenCalledTimes(1); }); }); it("does not delete from Settings when confirmation is canceled", async () => { mockConfirm.mockResolvedValueOnce(false); mockFetchAgent.mockResolvedValue(createMockAgent({ state: "idle" })); const addToast = vi.fn(); const onClose = vi.fn(); const user = userEvent.setup(); render( , ); await navigateToSettings(user); await user.click(await screen.findByRole("button", { name: "Delete Agent" })); await waitFor(() => { expect(mockConfirm).toHaveBeenCalled(); }); expect(mockDeleteAgent).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled(); expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("deleted"), "success"); }); it("shows settings delete control as unavailable for non-deletable states", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ state: "active" })); const user = userEvent.setup(); render( , ); await navigateToSettings(user); expect(await screen.findByRole("button", { name: "Delete Agent" })).toBeDisabled(); expect( screen.getByText("Agent deletion is only available when state is idle or paused (current state: active)."), ).toBeInTheDocument(); }); it("renders advanced settings form fields on Settings tab", async () => { const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { // Heartbeat Settings section expect(screen.getByLabelText("Heartbeat Interval (s)")).toBeInTheDocument(); expect(screen.getByLabelText("Heartbeat Timeout (s)")).toBeInTheDocument(); // Advanced Settings section expect(screen.getByLabelText("Max Retries")).toBeInTheDocument(); expect(screen.getByLabelText("Task Timeout (ms)")).toBeInTheDocument(); expect(screen.getByLabelText("Log Level")).toBeInTheDocument(); }); }); it("renders Reports To as a manager dropdown sourced from fetched agents", async () => { const user = userEvent.setup(); render( , ); await navigateToSettings(user); await waitFor(() => { expect(mockFetchAgents).toHaveBeenCalledWith(undefined, undefined); }); const reportsToSelect = await screen.findByLabelText("Reports To") as HTMLSelectElement; expect(reportsToSelect.tagName).toBe("SELECT"); const optionValues = Array.from(reportsToSelect.options).map((option) => option.value); expect(optionValues).toContain(""); expect(optionValues).toContain("agent-002"); expect(optionValues).toContain("agent-003"); expect(optionValues).not.toContain("agent-001"); }); it("shows existing reportsTo value as selected manager", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ reportsTo: "agent-003" } as any)); const user = userEvent.setup(); render( , ); await navigateToSettings(user); const reportsToSelect = await screen.findByLabelText("Reports To") as HTMLSelectElement; expect(reportsToSelect.value).toBe("agent-003"); }); it("preserves unknown reportsTo ids in dropdown until changed", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ reportsTo: "agent-missing" } as any)); const user = userEvent.setup(); render( , ); await navigateToSettings(user); const reportsToSelect = await screen.findByLabelText("Reports To") as HTMLSelectElement; expect(reportsToSelect.value).toBe("agent-missing"); expect(screen.getByRole("option", { name: "Unknown manager (agent-missing)" })).toBeInTheDocument(); }); it("saves selected manager id via updateAgent reportsTo", async () => { const user = userEvent.setup(); render( , ); await navigateToSettings(user); const reportsToSelect = await screen.findByLabelText("Reports To"); await user.selectOptions(reportsToSelect, "agent-003"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ reportsTo: "agent-003" }), undefined, ); }); }); it("clears reportsTo when selecting No manager", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ reportsTo: "agent-002" } as any)); const user = userEvent.setup(); render( , ); await navigateToSettings(user); const reportsToSelect = await screen.findByLabelText("Reports To"); await user.selectOptions(reportsToSelect, ""); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ reportsTo: undefined }), undefined, ); }); }); it("renders model settings section and pre-fills dropdown from runtimeConfig", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { modelProvider: "openai", modelId: "gpt-4o", }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect(screen.getByText("Model")).toBeInTheDocument(); }); const modelSelect = await screen.findByLabelText("Agent Model") as HTMLSelectElement; expect(modelSelect.value).toBe("openai/gpt-4o"); }); it("passes favorited providers and models to model dropdown", async () => { mockFetchModels.mockResolvedValueOnce({ models: [ { provider: "openai", id: "gpt-4o", name: "gpt-4o", reasoning: false, contextWindow: 128000 }, ], favoriteProviders: ["openai"], favoriteModels: ["openai/gpt-4o"], }); const user = userEvent.setup(); render( ); await navigateToSettings(user); const dropdown = await screen.findByTestId("custom-model-dropdown"); expect(dropdown).toHaveAttribute("data-favorite-providers", "openai"); expect(dropdown).toHaveAttribute("data-favorite-models", "openai/gpt-4o"); }); it("shows runtime mode selected when agent runtimeConfig has runtimeHint", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { runtimeHint: "openclaw", }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); const runtimeTab = screen.getByRole("tab", { name: "Plugin Runtime" }); expect(runtimeTab).toHaveAttribute("aria-selected", "true"); expect(screen.queryByLabelText("Agent Model")).toBeNull(); expect((screen.getByLabelText("Runtime") as HTMLSelectElement).value).toBe("openclaw"); }); it("saves selected model override as modelProvider/modelId/model in runtimeConfig", async () => { mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const modelSelect = await screen.findByLabelText("Agent Model"); await user.selectOptions(modelSelect, "anthropic/claude-3-7-sonnet"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ modelProvider: "anthropic", modelId: "claude-3-7-sonnet", model: "anthropic/claude-3-7-sonnet", }), }), undefined, ); }); }); it("saves selected plugin runtime as runtimeHint", async () => { mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); await user.click(screen.getByText("Plugin Runtime")); await user.selectOptions(screen.getByLabelText("Runtime"), "hermes"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ runtimeHint: "hermes", }), }), undefined, ); }); const payload = mockUpdateAgent.mock.calls[0][1] as { runtimeConfig: Record }; expect(payload.runtimeConfig.modelProvider).toBeUndefined(); expect(payload.runtimeConfig.modelId).toBeUndefined(); expect(payload.runtimeConfig.model).toBeUndefined(); }); it("clears model override from runtimeConfig when selecting global default", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { modelProvider: "openai", modelId: "gpt-4o", model: "openai/gpt-4o", heartbeatIntervalMs: 30000, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const modelSelect = await screen.findByLabelText("Agent Model"); await user.selectOptions(modelSelect, ""); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.not.objectContaining({ modelProvider: expect.anything(), modelId: expect.anything(), model: expect.anything(), }), }), undefined, ); }); }); it("shows empty fields when metadata and runtimeConfig are empty", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { const heartbeatInput = screen.getByLabelText("Heartbeat Interval (s)") as HTMLInputElement; expect(heartbeatInput.value).toBe(""); }); }); it("shows shared system default hint for heartbeat interval", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} })); const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); expect(heartbeatInput).toHaveAttribute("placeholder", String(DEFAULT_HEARTBEAT_INTERVAL_MS / 1000)); expect( screen.getByText(`How often heartbeats are checked. Leave empty for system default (${DEFAULT_HEARTBEAT_INTERVAL_MS / 1000}s / 1h).`), ).toBeInTheDocument(); }); it("pre-fills heartbeat fields from agent runtimeConfig", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: false, heartbeatIntervalMs: 15000, heartbeatTimeoutMs: 120000, }, metadata: { maxRetries: 5, logLevel: "debug", }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { const heartbeatEnabledInput = screen.getByLabelText("Heartbeat Enabled") as HTMLInputElement; expect(heartbeatEnabledInput.checked).toBe(false); const heartbeatInput = screen.getByLabelText("Heartbeat Interval (s)") as HTMLInputElement; expect(heartbeatInput.value).toBe("15"); const heartbeatTimeoutInput = screen.getByLabelText("Heartbeat Timeout (s)") as HTMLInputElement; expect(heartbeatTimeoutInput.value).toBe("120"); const retriesInput = screen.getByLabelText("Max Retries") as HTMLInputElement; expect(retriesInput.value).toBe("5"); const logLevelSelect = screen.getByLabelText("Log Level") as HTMLSelectElement; expect(logLevelSelect.value).toBe("debug"); }); }); it("defaults heartbeat toggle to enabled when runtimeConfig.enabled is missing", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Heartbeat Enabled") as HTMLInputElement).checked).toBe(true); }); }); it("defaults auto-claim toggle to enabled when runtimeConfig.autoClaimRelevantTasks is missing", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Auto-Claim Relevant Tasks") as HTMLInputElement).checked).toBe(true); }); }); it("shows Save Settings button disabled when no changes", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect(screen.getByText("Save Settings")).toBeDisabled(); }); }); it("keeps Save Settings disabled when heartbeat runtimeConfig values are pre-filled and unchanged", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {}, runtimeConfig: { heartbeatIntervalMs: 30000, heartbeatTimeoutMs: 60000, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Heartbeat Interval (s)") as HTMLInputElement).value).toBe("30"); expect((screen.getByLabelText("Heartbeat Timeout (s)") as HTMLInputElement).value).toBe("60"); expect(screen.getByText("Save Settings")).toBeDisabled(); }); }); it("enables Save Settings when a field is changed", async () => { const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); await user.clear(heartbeatInput); await user.type(heartbeatInput, "15"); await waitFor(() => { expect(screen.getByText("Save Settings")).not.toBeDisabled(); }); }); it("shows validation error for non-numeric input in number field", async () => { const user = userEvent.setup(); render( ); await navigateToSettings(user); // Simulate setting a non-numeric value via React's internal value setter // (userEvent.type on type="number" rejects non-numeric chars, so we bypass it) const heartbeatInput = (await screen.findByLabelText("Heartbeat Interval (s)")) as HTMLInputElement; const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' )?.set; nativeInputValueSetter?.call(heartbeatInput, 'abc'); fireEvent.change(heartbeatInput); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(screen.getByText(/must be a valid number/)).toBeInTheDocument(); }); }); it("shows validation error for number below minimum", async () => { const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatTimeoutInput = await screen.findByLabelText("Heartbeat Timeout (s)"); await user.clear(heartbeatTimeoutInput); await user.type(heartbeatTimeoutInput, "4"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(screen.getByText(/must be at least 5/)).toBeInTheDocument(); }); }); it("shows validation error for number above maximum", async () => { const user = userEvent.setup(); render( ); await navigateToSettings(user); const retriesInput = await screen.findByLabelText("Max Retries"); await user.clear(retriesInput); await user.type(retriesInput, "99"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(screen.getByText(/must be at most 10/)).toBeInTheDocument(); }); }); it("calls updateAgent with correct metadata and runtimeConfig on save", async () => { const addToast = vi.fn(); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); await user.clear(heartbeatInput); await user.type(heartbeatInput, "15"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ metadata: expect.any(Object), runtimeConfig: expect.objectContaining({ heartbeatIntervalMs: 15000 }), }), undefined, ); }); expect(addToast).toHaveBeenCalledWith("Settings saved", "success"); }); it("persists heartbeat enabled toggle changes on save", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: true, heartbeatIntervalMs: 30000, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatEnabledInput = await screen.findByLabelText("Heartbeat Enabled"); await user.click(heartbeatEnabledInput); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ enabled: false, heartbeatIntervalMs: 30000 }), }), undefined, ); }); }); it("defaults run-missed-heartbeat-on-startup toggle to disabled when runtimeConfig flag is missing", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Run Missed Heartbeat On Startup") as HTMLInputElement).checked).toBe(false); }); }); it("persists run-missed-heartbeat-on-startup toggle changes on save", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: true, heartbeatIntervalMs: 30000, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const toggle = await screen.findByLabelText("Run Missed Heartbeat On Startup"); await user.click(toggle); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ runMissedHeartbeatOnStartup: true }), }), undefined, ); }); }); it("persists auto-claim toggle changes on save", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: true, autoClaimRelevantTasks: true, heartbeatIntervalMs: 30000, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const autoClaimInput = await screen.findByLabelText("Auto-Claim Relevant Tasks"); await user.click(autoClaimInput); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ autoClaimRelevantTasks: false }), }), undefined, ); }); }); it("applies coordination-only preset and persists disabled auto-claim", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: true, autoClaimRelevantTasks: true, autoClaimCandidatesInPrompt: 5, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( , ); await navigateToSettings(user); await user.click(await screen.findByRole("button", { name: "Apply preset" })); expect((screen.getByLabelText("Auto-Claim Relevant Tasks") as HTMLInputElement).checked).toBe(false); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ autoClaimRelevantTasks: false, autoClaimCandidatesInPrompt: 0, }), }), undefined, ); }); }); it("defaults allow-parallel-execution toggle to checked when runtimeConfig.allowParallelExecution is undefined", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Allow Parallel Execution") as HTMLInputElement).checked).toBe(true); }); }); it("defaults allow-parallel-execution toggle to unchecked when runtimeConfig.allowParallelExecution === false", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, allowParallelExecution: false, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Allow Parallel Execution") as HTMLInputElement).checked).toBe(false); }); }); it("defaults allow-parallel-execution toggle to checked when runtimeConfig.allowParallelExecution === true", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, allowParallelExecution: true, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText("Allow Parallel Execution") as HTMLInputElement).checked).toBe(true); }); }); it("persists allow-parallel-execution toggle changes on save", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: true, allowParallelExecution: true, heartbeatIntervalMs: 30000, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const toggle = await screen.findByLabelText("Allow Parallel Execution"); await user.click(toggle); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ allowParallelExecution: false }), }), undefined, ); }); }); it("defaults skip-heartbeat-when-idle toggle to unchecked when runtimeConfig.skipHeartbeatWhenIdle is undefined", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText(/skip heartbeat when idle/i) as HTMLInputElement).checked).toBe(false); }); }); it("defaults skip-heartbeat-when-idle toggle to unchecked when runtimeConfig.skipHeartbeatWhenIdle === false", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, skipHeartbeatWhenIdle: false, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText(/skip heartbeat when idle/i) as HTMLInputElement).checked).toBe(false); }); }); it("defaults skip-heartbeat-when-idle toggle to checked when runtimeConfig.skipHeartbeatWhenIdle === true", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000, skipHeartbeatWhenIdle: true, }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); await waitFor(() => { expect((screen.getByLabelText(/skip heartbeat when idle/i) as HTMLInputElement).checked).toBe(true); }); }); it("persists skip-heartbeat-when-idle toggle changes on save", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { enabled: true, skipHeartbeatWhenIdle: false, heartbeatIntervalMs: 30000, }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const toggle = await screen.findByLabelText(/skip heartbeat when idle/i); await user.click(toggle); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.objectContaining({ skipHeartbeatWhenIdle: true }), }), undefined, ); }); }); it("forwards projectId to updateAgent", async () => { const addToast = vi.fn(); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); await user.clear(heartbeatInput); await user.type(heartbeatInput, "20"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ metadata: expect.any(Object) }), "proj_456", ); }); }); it("re-fetches agent after successful save", async () => { const addToast = vi.fn(); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); // Initial fetch + save-triggered refetch const initialFetchCount = mockFetchAgent.mock.calls.length; const retriesInput = await screen.findByLabelText("Max Retries"); await user.clear(retriesInput); await user.type(retriesInput, "7"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockFetchAgent.mock.calls.length).toBeGreaterThan(initialFetchCount); }); }); it("shows error toast on save failure", async () => { const addToast = vi.fn(); mockUpdateAgent.mockRejectedValue(new Error("Network error")); const user = userEvent.setup(); render( ); await navigateToSettings(user); const retriesInput = await screen.findByLabelText("Max Retries"); await user.clear(retriesInput); await user.type(retriesInput, "2"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(addToast).toHaveBeenCalledWith( expect.stringContaining("Failed to save settings"), "error", ); }); }); it("shows validation error for non-numeric input in number field", async () => { const user = userEvent.setup(); render( ); await navigateToSettings(user); // Type "abc" directly into a text input const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); await user.clear(heartbeatInput); await user.type(heartbeatInput, "abc"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(screen.getByText(/must be a valid number/)).toBeInTheDocument(); }); }); it("pre-fills and persists logLevel select field", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: { logLevel: "debug" }, })); const user = userEvent.setup(); render( ); await navigateToSettings(user); const logLevelSelect = await screen.findByLabelText("Log Level"); expect((logLevelSelect as HTMLSelectElement).value).toBe("debug"); }); it("clears runtimeConfig key when heartbeat field is cleared to empty", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ runtimeConfig: { heartbeatIntervalMs: 30000 }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); expect((heartbeatInput as HTMLInputElement).value).toBe("30"); await user.clear(heartbeatInput); await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ runtimeConfig: expect.not.objectContaining({ heartbeatIntervalMs: expect.anything() }), }), undefined, ); }); }); it("persists existing non-advanced metadata keys and runtimeConfig during save", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: { customKey: "preserved" }, runtimeConfig: { enabled: true, heartbeatIntervalMs: 30000, otherConfig: "also-preserved" }, })); mockUpdateAgent.mockResolvedValue(createMockAgent() as any); const user = userEvent.setup(); render( ); await navigateToSettings(user); const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (s)"); await user.clear(heartbeatInput); await user.type(heartbeatInput, "45"); await user.click(screen.getByText("Save Settings")); await waitFor(() => { const call = mockUpdateAgent.mock.calls[0]; const payload = (call as any)[1]; expect(payload.metadata.customKey).toBe("preserved"); expect(payload.runtimeConfig.enabled).toBe(true); expect(payload.runtimeConfig.heartbeatIntervalMs).toBe(45000); expect(payload.runtimeConfig.otherConfig).toBe("also-preserved"); }); }); }); // ── Budget Settings ────────────────────────────────────────────────────── });