import { beforeEach, describe, expect, it, vi } from "vitest"; import { PrimariaApi } from "../../api/api"; import { PluginBusyList } from "../../api/plugin-busy-manager/plugin-busy-list/component"; import { disposeShell, raiseCloseEvent, raiseCustomCloseEvent } from "../../disposer"; import { disposePlugins } from "../../handle-plugins"; import { ExitShellHandler } from "./handler"; import { ExitShell } from "./request"; vi.mock("../../handle-plugins", () => ({ disposePlugins: vi.fn(), })); vi.mock("../../disposer", () => ({ disposeShell: vi.fn(), raiseCloseEvent: vi.fn(), raiseCustomCloseEvent: vi.fn(), })); const createMockApi = (): PrimariaApi => ({ exitGuardManager: { canExit: vi.fn().mockResolvedValue(true), }, pluginBusyManager: { getTasks: vi.fn().mockReturnValue([]), }, interactionService: { confirm: vi.fn(), }, notificationService: { error: vi.fn(), }, }) as any; describe("ExitShellHandler", () => { let handler: ExitShellHandler; let mockApi: PrimariaApi; beforeEach(() => { mockApi = createMockApi(); handler = new ExitShellHandler(mockApi); vi.clearAllMocks(); }); it("disposes and raises custom close event when canExit resolves true and message has ecapEvent", async () => { const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (disposePlugins as any).mockResolvedValue(undefined); await handler.handle(message); expect(mockApi.exitGuardManager.canExit).toHaveBeenCalled(); expect(disposePlugins).toHaveBeenCalled(); expect(disposeShell).toHaveBeenCalled(); expect(raiseCustomCloseEvent).toHaveBeenCalledWith(message); }); it("raises default close event when no ecapEvent is present", async () => { const message = {} as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (disposePlugins as any).mockResolvedValue(undefined); await handler.handle(message); expect(disposePlugins).toHaveBeenCalled(); expect(disposeShell).toHaveBeenCalled(); expect(raiseCloseEvent).toHaveBeenCalled(); expect(raiseCustomCloseEvent).not.toHaveBeenCalled(); }); it("aborts the exit when any guard returns false", async () => { const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(false); await handler.handle(message); expect(mockApi.exitGuardManager.canExit).toHaveBeenCalled(); expect(disposePlugins).not.toHaveBeenCalled(); expect(disposeShell).not.toHaveBeenCalled(); expect(raiseCustomCloseEvent).not.toHaveBeenCalled(); expect(raiseCloseEvent).not.toHaveBeenCalled(); }); it("notifies and raises custom close event when dispose throws and ecapEvent is present", async () => { const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (disposePlugins as any).mockRejectedValue(new Error("boom")); await handler.handle(message); expect(mockApi.notificationService.error).toHaveBeenCalledWith("errors.exit"); expect(raiseCustomCloseEvent).toHaveBeenCalledWith(message); expect(raiseCloseEvent).not.toHaveBeenCalled(); }); it("notifies and raises default close event when dispose throws and no ecapEvent", async () => { const message = {} as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (disposePlugins as any).mockRejectedValue(new Error("boom")); await handler.handle(message); expect(mockApi.notificationService.error).toHaveBeenCalledWith("errors.exit"); expect(raiseCloseEvent).toHaveBeenCalled(); expect(raiseCustomCloseEvent).not.toHaveBeenCalled(); }); it("proceeds if disposePlugins exceeds the 10s timeout", async () => { const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (disposePlugins as any).mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 11000))); await handler.handle(message); expect(disposePlugins).toHaveBeenCalled(); expect(disposeShell).toHaveBeenCalled(); expect(raiseCustomCloseEvent).toHaveBeenCalledWith(message); }, 15000); it("raises raiseCloseEvent when no exitEvent is passed", async () => { (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (disposePlugins as any).mockResolvedValue(undefined); await handler.handle(undefined as any); expect(disposeShell).toHaveBeenCalled(); expect(raiseCloseEvent).toHaveBeenCalled(); expect(raiseCustomCloseEvent).not.toHaveBeenCalled(); }); describe("legacy pluginBusyManager fallback", () => { it("asks for confirmation when canExit allows but legacy busy tasks exist", async () => { const busyTasks = [{ taskId: "t1", taskDescription: "saving draft" }]; const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (mockApi.pluginBusyManager.getTasks as any).mockReturnValue(busyTasks); (mockApi.interactionService.confirm as any).mockResolvedValue({ confirmed: true }); (disposePlugins as any).mockResolvedValue(undefined); await handler.handle(message); expect(mockApi.interactionService.confirm).toHaveBeenCalledWith( { busyTasks }, { component: PluginBusyList }, { title: "actions.askExit", state: "error", confirmButtonText: "Sí", cancelButtonText: "No", }, ); expect(disposePlugins).toHaveBeenCalled(); expect(disposeShell).toHaveBeenCalled(); expect(raiseCustomCloseEvent).toHaveBeenCalledWith(message); }); it("aborts the exit when the legacy busy-task confirmation is declined", async () => { const busyTasks = [{ taskId: "t1", taskDescription: "saving draft" }]; const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true); (mockApi.pluginBusyManager.getTasks as any).mockReturnValue(busyTasks); (mockApi.interactionService.confirm as any).mockResolvedValue({ confirmed: false }); await handler.handle(message); expect(mockApi.interactionService.confirm).toHaveBeenCalled(); expect(disposePlugins).not.toHaveBeenCalled(); expect(disposeShell).not.toHaveBeenCalled(); expect(raiseCustomCloseEvent).not.toHaveBeenCalled(); expect(raiseCloseEvent).not.toHaveBeenCalled(); }); it("does not consult legacy busy tasks when an ExitGuard already vetoed the exit", async () => { const message = { ecapEvent: {} } as ExitShell; (mockApi.exitGuardManager.canExit as any).mockResolvedValue(false); await handler.handle(message); expect(mockApi.pluginBusyManager.getTasks).not.toHaveBeenCalled(); expect(mockApi.interactionService.confirm).not.toHaveBeenCalled(); }); }); });