import { beforeEach, describe, expect, it, vi } from 'vitest'; const hostApiFetchMock = vi.fn(); const subscribeHostEventMock = vi.fn(); const chatStateMock = vi.hoisted(() => ({ state: { currentSessionKey: 'session-1', sessions: [] as Array<{ key: string }>, sending: true, activeRunId: 'run-1' as string | null, pendingFinal: true, lastUserMessageAt: 123, error: null as string | null, loadHistory: vi.fn(), loadSessions: vi.fn(), handleChatEvent: vi.fn(), }, })); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); vi.mock('@/lib/host-events', () => ({ subscribeHostEvent: (...args: unknown[]) => subscribeHostEventMock(...args), })); vi.mock('@/stores/chat', () => ({ useChatStore: { getState: () => chatStateMock.state, setState: (patch: Record) => { Object.assign(chatStateMock.state, patch); }, }, })); describe('gateway store event wiring', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); Object.assign(chatStateMock.state, { currentSessionKey: 'session-1', sessions: [], sending: true, activeRunId: 'run-1', pendingFinal: true, lastUserMessageAt: 123, error: null, }); }); it('subscribes to host events through subscribeHostEvent on init', async () => { hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 }); const handlers = new Map void>(); subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => { handlers.set(eventName, handler); return () => {}; }); const { useGatewayStore } = await import('@/stores/gateway'); await useGatewayStore.getState().init(); expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:status', expect.any(Function)); expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:error', expect.any(Function)); expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:notification', expect.any(Function)); expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:chat-message', expect.any(Function)); expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:channel-status', expect.any(Function)); handlers.get('gateway:status')?.({ state: 'stopped', port: 18789 }); expect(useGatewayStore.getState().status.state).toBe('stopped'); }); it('propagates gatewayReady field from status events', async () => { hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789, gatewayReady: false }); const handlers = new Map void>(); subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => { handlers.set(eventName, handler); return () => {}; }); const { useGatewayStore } = await import('@/stores/gateway'); await useGatewayStore.getState().init(); // Initially gatewayReady=false from the status fetch expect(useGatewayStore.getState().status.gatewayReady).toBe(false); // Simulate gateway.ready event setting gatewayReady=true handlers.get('gateway:status')?.({ state: 'running', port: 18789, gatewayReady: true }); expect(useGatewayStore.getState().status.gatewayReady).toBe(true); }); it('treats undefined gatewayReady as ready for backwards compatibility', async () => { hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 }); const handlers = new Map void>(); subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => { handlers.set(eventName, handler); return () => {}; }); const { useGatewayStore } = await import('@/stores/gateway'); await useGatewayStore.getState().init(); const status = useGatewayStore.getState().status; // gatewayReady is undefined (old gateway version) — should be treated as ready expect(status.gatewayReady).toBeUndefined(); expect(status.state === 'running' && status.gatewayReady !== false).toBe(true); }); it('does not clear sending state on intermediate agent phase=end notifications', async () => { hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 }); const handlers = new Map void>(); subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => { handlers.set(eventName, handler); return () => {}; }); const { useGatewayStore } = await import('@/stores/gateway'); await useGatewayStore.getState().init(); handlers.get('gateway:notification')?.({ method: 'agent', params: { phase: 'end', runId: 'run-1', sessionKey: 'session-1', }, }); await vi.waitFor(() => { expect(chatStateMock.state.loadHistory).toHaveBeenCalled(); }); expect(chatStateMock.state.sending).toBe(true); expect(chatStateMock.state.activeRunId).toBe('run-1'); expect(chatStateMock.state.pendingFinal).toBe(true); }); it('clears sending state on terminal agent completion notifications', async () => { hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 }); const handlers = new Map void>(); subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => { handlers.set(eventName, handler); return () => {}; }); const { useGatewayStore } = await import('@/stores/gateway'); await useGatewayStore.getState().init(); handlers.get('gateway:notification')?.({ method: 'agent', params: { phase: 'completed', runId: 'run-1', sessionKey: 'session-1', }, }); await vi.waitFor(() => { expect(chatStateMock.state.sending).toBe(false); }); expect(chatStateMock.state.activeRunId).toBeNull(); expect(chatStateMock.state.pendingFinal).toBe(false); }); });