test(desktop): Phase 3 store unit tests — 112 new tests for 5 stores
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- saasStore: login/logout/register, TOTP setup/verify/disable, billing (plans/subscription/payment), templates, connection mode, config sync - workflowStore: CRUD, trigger, cancel, loadRuns, client injection - offlineStore: queue message, update/remove, reconnect backoff, getters - handStore: loadHands, getHandDetails, trigger/approve/cancel, triggers CRUD, approvals, autonomy blocking - streamStore: chatMode switching, getChatModeConfig, suggestions, setIsLoading, cancelStream, searchSkills, initStreamListener All 173 tests pass (61 existing + 112 new).
This commit is contained in:
319
desktop/tests/store/streamStore.test.ts
Normal file
319
desktop/tests/store/streamStore.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user