import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('electron', () => ({ app: { getPath: () => '/tmp', isPackaged: false, }, utilityProcess: { fork: vi.fn(), }, })); describe('GatewayManager heartbeat recovery', () => { const originalPlatform = process.platform; beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); vi.useFakeTimers(); vi.setSystemTime(new Date('2026-03-19T00:00:00.000Z')); Object.defineProperty(process, 'platform', { value: originalPlatform }); }); afterEach(() => { vi.useRealTimers(); Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('restarts after consecutive heartbeat misses reach threshold', async () => { const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); const ws = { readyState: 1, // WebSocket.OPEN ping: vi.fn(), terminate: vi.fn(), on: vi.fn(), }; (manager as unknown as { ws: typeof ws }).ws = ws; (manager as unknown as { shouldReconnect: boolean }).shouldReconnect = true; (manager as unknown as { status: { state: string; port: number } }).status = { state: 'running', port: 18789, }; const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); vi.advanceTimersByTime(120_000); expect(ws.ping).toHaveBeenCalledTimes(3); expect(ws.terminate).not.toHaveBeenCalled(); expect(restartSpy).toHaveBeenCalledTimes(1); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); it('does not restart when heartbeat is recovered by incoming messages', async () => { const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); const ws = { readyState: 1, // WebSocket.OPEN ping: vi.fn(), terminate: vi.fn(), on: vi.fn(), }; (manager as unknown as { ws: typeof ws }).ws = ws; (manager as unknown as { shouldReconnect: boolean }).shouldReconnect = true; (manager as unknown as { status: { state: string; port: number } }).status = { state: 'running', port: 18789, }; const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); vi.advanceTimersByTime(30_000); // ping #1 vi.advanceTimersByTime(30_000); // miss #1 + ping #2 (manager as unknown as { handleMessage: (message: unknown) => void }).handleMessage('alive'); vi.advanceTimersByTime(30_000); // recovered, ping #3 vi.advanceTimersByTime(30_000); // miss #1 + ping #4 vi.advanceTimersByTime(30_000); // miss #2 + ping #5 expect(ws.terminate).not.toHaveBeenCalled(); expect(restartSpy).not.toHaveBeenCalled(); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); it('skips heartbeat recovery when auto-reconnect is disabled', async () => { const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); const ws = { readyState: 1, ping: vi.fn(), terminate: vi.fn(), on: vi.fn(), }; (manager as unknown as { ws: typeof ws }).ws = ws; (manager as unknown as { shouldReconnect: boolean }).shouldReconnect = false; (manager as unknown as { status: { state: string; port: number } }).status = { state: 'running', port: 18789, }; const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); vi.advanceTimersByTime(120_000); expect(restartSpy).not.toHaveBeenCalled(); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); it('keeps heartbeat recovery disabled on windows', async () => { Object.defineProperty(process, 'platform', { value: 'win32' }); const { GatewayManager } = await import('@electron/gateway/manager'); const manager = new GatewayManager(); const ws = { readyState: 1, ping: vi.fn(), terminate: vi.fn(), on: vi.fn(), }; (manager as unknown as { ws: typeof ws }).ws = ws; (manager as unknown as { shouldReconnect: boolean }).shouldReconnect = true; (manager as unknown as { status: { state: string; port: number } }).status = { state: 'running', port: 18789, }; const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue(); (manager as unknown as { startPing: () => void }).startPing(); vi.advanceTimersByTime(400_000); expect(restartSpy).not.toHaveBeenCalled(); (manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear(); }); });