import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import { EngineControlMenu } from "../EngineControlMenu"; const defaultSettings = { maxConcurrent: 2, maxTriageConcurrent: 1, maxWorktrees: 4, globalPause: false, enginePaused: false, autoMerge: true, experimentalFeatures: {}, }; const apiMocks = vi.hoisted(() => ({ fetchConfig: vi.fn(), fetchSettings: vi.fn(), updateSettings: vi.fn(), updateGlobalSettings: vi.fn(), })); const legacyMocks = vi.hoisted(() => ({ fetchConfig: vi.fn(), fetchSettings: vi.fn(), updateSettings: vi.fn(), })); vi.mock("../../api", () => apiMocks); vi.mock("../../api/legacy", () => legacyMocks); vi.mock("../../versionCheck", () => ({ setAutoReloadEnabled: vi.fn(), })); async function openMenu() { render(); fireEvent.click(screen.getByTestId("engine-control-menu-trigger")); await screen.findByTestId("engine-control-menu"); } describe("EngineControlMenu", () => { beforeEach(() => { vi.useRealTimers(); apiMocks.fetchConfig.mockResolvedValue({ maxConcurrent: 2, rootDir: "/workspace/project" }); apiMocks.fetchSettings.mockResolvedValue({ ...defaultSettings }); apiMocks.updateSettings.mockResolvedValue({ ...defaultSettings }); apiMocks.updateGlobalSettings.mockResolvedValue({}); legacyMocks.fetchConfig.mockResolvedValue({ maxConcurrent: 2, rootDir: "/workspace/project" }); legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings }); legacyMocks.updateSettings.mockResolvedValue({ ...defaultSettings }); }); afterEach(() => { vi.clearAllMocks(); vi.useRealTimers(); }); it("stops and starts the global AI engine via settings", async () => { apiMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, globalPause: false }); await openMenu(); fireEvent.click(screen.getByTestId("engine-control-stop-btn")); await waitFor(() => expect(apiMocks.updateSettings).toHaveBeenCalledWith( { globalPause: true, globalPauseReason: "manual" }, "proj_123", )); }); it("starts the global AI engine when currently stopped", async () => { apiMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, globalPause: true }); await openMenu(); await waitFor(() => expect(screen.getByTestId("engine-control-stop-btn")).toHaveTextContent(/start ai engine/i)); fireEvent.click(screen.getByTestId("engine-control-stop-btn")); await waitFor(() => expect(apiMocks.updateSettings).toHaveBeenCalledWith( { globalPause: false, globalPauseReason: undefined }, "proj_123", )); }); it("pauses and resumes triage, and disables triage while globally stopped", async () => { apiMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, enginePaused: false }); await openMenu(); fireEvent.click(screen.getByTestId("engine-control-pause-triage-btn")); await waitFor(() => expect(apiMocks.updateSettings).toHaveBeenCalledWith({ enginePaused: true }, "proj_123")); vi.clearAllMocks(); apiMocks.fetchConfig.mockResolvedValue({ maxConcurrent: 2, rootDir: "/workspace/project" }); apiMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, globalPause: true, enginePaused: true }); legacyMocks.fetchConfig.mockResolvedValue({ maxConcurrent: 2, rootDir: "/workspace/project" }); legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings }); render(); fireEvent.click(screen.getAllByTestId("engine-control-menu-trigger")[1]); await waitFor(() => expect(screen.getAllByTestId("engine-control-pause-triage-btn")).toHaveLength(2)); const pauseButton = screen.getAllByTestId("engine-control-pause-triage-btn")[1]; expect(pauseButton).toBeDisabled(); expect(pauseButton).toHaveTextContent(/resume scheduling/i); }); it("persists debounced concurrency and worktree slider changes and refreshes settings", async () => { legacyMocks.fetchSettings.mockResolvedValue({ ...defaultSettings, maxConcurrent: 12, maxTriageConcurrent: 3, maxWorktrees: 25, }); await openMenu(); const maxConcurrent = await screen.findByLabelText(/max concurrent tasks/i); const maxTriage = screen.getByLabelText(/max triage concurrent/i); const maxWorktrees = screen.getByLabelText(/max worktrees/i); vi.useFakeTimers(); expect(maxConcurrent).toHaveAttribute("max", "12"); expect(maxConcurrent).toHaveValue("12"); expect(maxWorktrees).toHaveAttribute("max", "25"); expect(maxWorktrees).toHaveValue("25"); fireEvent.change(maxConcurrent, { target: { value: "9" } }); fireEvent.change(maxTriage, { target: { value: "4" } }); fireEvent.change(maxWorktrees, { target: { value: "8" } }); await act(async () => { await vi.advanceTimersByTimeAsync(500); }); expect(legacyMocks.updateSettings).toHaveBeenCalledWith( { maxConcurrent: 9, maxTriageConcurrent: 4, maxWorktrees: 8 }, "proj_123", ); expect(apiMocks.fetchSettings).toHaveBeenCalledTimes(2); }); it("renders a load error state without crashing", async () => { legacyMocks.fetchSettings.mockRejectedValue(new Error("settings unavailable")); await openMenu(); expect(await screen.findByRole("alert")).toHaveTextContent("settings unavailable"); }); });