/** * Stream Store Tests * * Tests for chat mode management, follow-up suggestions, * cancel stream, and skill search. * * Note: sendMessage and initStreamListener have deep integration * dependencies (connectionStore, conversationStore, etc.) and are * tested indirectly through chatStore.test.ts. This file tests * the standalone actions. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { localStorageMock } from '../setup'; // ── Mock all external dependencies ── vi.mock('../../src/store/connectionStore', () => ({ getClient: vi.fn(() => ({ chatStream: vi.fn(), chat: vi.fn(), onAgentStream: vi.fn(() => () => {}), getState: vi.fn(() => 'disconnected'), cancelStream: vi.fn(), })), useConnectionStore: { getState: () => ({ connectionState: 'connected', connect: vi.fn(), }), setState: vi.fn(), subscribe: () => () => {}, }, })); vi.mock('../../src/store/chat/conversationStore', () => ({ useConversationStore: { getState: () => ({ currentAgent: { id: '1', name: 'ZCLAW' }, sessionKey: null, currentConversationId: null, agents: [{ id: '1', name: 'ZCLAW' }], }), setState: vi.fn(), subscribe: () => () => {}, }, resolveGatewayAgentId: vi.fn(() => 'agent-1'), })); vi.mock('../../src/store/chat/messageStore', () => ({ useMessageStore: { getState: () => ({ totalInputTokens: 0, totalOutputTokens: 0, addTokenUsage: vi.fn(), }), }, })); vi.mock('../../src/store/chat/artifactStore', () => ({ useArtifactStore: { getState: () => ({ addArtifact: vi.fn(), }), }, })); vi.mock('../../src/store/offlineStore', () => ({ useOfflineStore: { getState: () => ({ queueMessage: vi.fn(() => 'queued_123'), }), }, isOffline: vi.fn(() => false), })); vi.mock('../../src/lib/gateway-client', () => ({ getGatewayClient: vi.fn(), })); vi.mock('../../src/lib/intelligence-client', () => ({ intelligenceClient: { compactor: { checkThreshold: vi.fn(), compact: vi.fn(), }, memory: { search: vi.fn(), }, identity: { buildPrompt: vi.fn(), }, reflection: { recordConversation: vi.fn(() => Promise.resolve()), shouldReflect: vi.fn(() => Promise.resolve(false)), reflect: vi.fn(), }, }, })); vi.mock('../../src/lib/memory-extractor', () => ({ getMemoryExtractor: vi.fn(() => ({ extractFromConversation: vi.fn(() => Promise.resolve([])), })), })); vi.mock('../../src/lib/skill-discovery', () => ({ getSkillDiscovery: vi.fn(() => ({ searchSkills: vi.fn(() => ({ results: [], totalAvailable: 0 })), })), })); vi.mock('../../src/lib/speech-synth', () => ({ speechSynth: { speak: vi.fn(() => Promise.resolve()), }, })); vi.mock('../../src/lib/crypto-utils', () => ({ generateRandomString: (len: number) => 'x'.repeat(len), })); vi.mock('../../src/lib/logger', () => ({ createLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }), })); // CHAT_MODES mock - must match the actual component structure vi.mock('../../src/components/ai', () => ({ CHAT_MODES: { thinking: { config: { thinking_enabled: true, reasoning_effort: 'high', plan_mode: false, subagent_enabled: false }, }, normal: { config: { thinking_enabled: false, reasoning_effort: 'medium', plan_mode: false, subagent_enabled: false }, }, agent: { config: { thinking_enabled: true, reasoning_effort: 'high', plan_mode: true, subagent_enabled: true }, }, }, })); import { useStreamStore } from '../../src/store/chat/streamStore'; beforeEach(() => { vi.clearAllMocks(); localStorageMock.clear(); useStreamStore.setState({ isStreaming: false, isLoading: false, chatMode: 'thinking', suggestions: [], activeRunId: null, }); }); // ═══════════════════════════════════════════════════════════════════ // Initial State // ═══════════════════════════════════════════════════════════════════ describe('streamStore — initial state', () => { it('should not be streaming', () => { expect(useStreamStore.getState().isStreaming).toBe(false); }); it('should have thinking chat mode', () => { expect(useStreamStore.getState().chatMode).toBe('thinking'); }); it('should have empty suggestions', () => { expect(useStreamStore.getState().suggestions).toEqual([]); }); it('should have no active run id', () => { expect(useStreamStore.getState().activeRunId).toBeNull(); }); it('should not be loading', () => { expect(useStreamStore.getState().isLoading).toBe(false); }); }); // ═══════════════════════════════════════════════════════════════════ // Chat Mode // ═══════════════════════════════════════════════════════════════════ describe('streamStore — setChatMode', () => { it('should switch to normal mode', () => { useStreamStore.getState().setChatMode('normal'); expect(useStreamStore.getState().chatMode).toBe('normal'); }); it('should switch to agent mode', () => { useStreamStore.getState().setChatMode('agent'); expect(useStreamStore.getState().chatMode).toBe('agent'); }); it('should switch back to thinking mode', () => { useStreamStore.getState().setChatMode('agent'); useStreamStore.getState().setChatMode('thinking'); expect(useStreamStore.getState().chatMode).toBe('thinking'); }); }); // ═══════════════════════════════════════════════════════════════════ // getChatModeConfig // ═══════════════════════════════════════════════════════════════════ describe('streamStore — getChatModeConfig', () => { it('should return thinking config by default', () => { const config = useStreamStore.getState().getChatModeConfig(); expect(config.thinking_enabled).toBe(true); expect(config.reasoning_effort).toBe('high'); }); it('should return normal config', () => { useStreamStore.getState().setChatMode('normal'); const config = useStreamStore.getState().getChatModeConfig(); expect(config.thinking_enabled).toBe(false); expect(config.reasoning_effort).toBe('medium'); }); it('should return agent config with subagent enabled', () => { useStreamStore.getState().setChatMode('agent'); const config = useStreamStore.getState().getChatModeConfig(); expect(config.subagent_enabled).toBe(true); expect(config.plan_mode).toBe(true); }); }); // ═══════════════════════════════════════════════════════════════════ // Suggestions // ═══════════════════════════════════════════════════════════════════ describe('streamStore — suggestions', () => { it('should set suggestions', () => { useStreamStore.getState().setSuggestions(['Suggestion 1', 'Suggestion 2']); expect(useStreamStore.getState().suggestions).toEqual(['Suggestion 1', 'Suggestion 2']); }); it('should replace previous suggestions', () => { useStreamStore.getState().setSuggestions(['Old']); useStreamStore.getState().setSuggestions(['New 1', 'New 2']); expect(useStreamStore.getState().suggestions).toEqual(['New 1', 'New 2']); }); it('should clear suggestions with empty array', () => { useStreamStore.getState().setSuggestions(['Something']); useStreamStore.getState().setSuggestions([]); expect(useStreamStore.getState().suggestions).toEqual([]); }); }); // ═══════════════════════════════════════════════════════════════════ // setIsLoading // ═══════════════════════════════════════════════════════════════════ describe('streamStore — setIsLoading', () => { it('should set loading state', () => { useStreamStore.getState().setIsLoading(true); expect(useStreamStore.getState().isLoading).toBe(true); }); it('should unset loading state', () => { useStreamStore.getState().setIsLoading(true); useStreamStore.getState().setIsLoading(false); expect(useStreamStore.getState().isLoading).toBe(false); }); }); // ═══════════════════════════════════════════════════════════════════ // cancelStream (without chatStore injection) // ═══════════════════════════════════════════════════════════════════ describe('streamStore — cancelStream', () => { it('should do nothing when not streaming', () => { useStreamStore.getState().cancelStream(); // No crash, state unchanged expect(useStreamStore.getState().isStreaming).toBe(false); }); it('should not crash when no chatStore injected', () => { useStreamStore.setState({ isStreaming: true, activeRunId: 'run-123' }); // Without _chat injected, cancelStream returns early expect(() => useStreamStore.getState().cancelStream()).not.toThrow(); }); }); // ═══════════════════════════════════════════════════════════════════ // searchSkills // ═══════════════════════════════════════════════════════════════════ describe('streamStore — searchSkills', () => { it('should return search results', () => { const result = useStreamStore.getState().searchSkills('test query'); expect(result).toHaveProperty('results'); expect(result).toHaveProperty('totalAvailable'); expect(Array.isArray(result.results)).toBe(true); }); }); // ═══════════════════════════════════════════════════════════════════ // initStreamListener // ═══════════════════════════════════════════════════════════════════ describe('streamStore — initStreamListener', () => { it('should return unsubscribe function', () => { const unsubscribe = useStreamStore.getState().initStreamListener(); expect(typeof unsubscribe).toBe('function'); unsubscribe(); }); });