Files
zclaw_openfang/desktop/tests/store/streamStore.test.ts
iven d758a4477f
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
test(desktop): Phase 3 store unit tests — 112 new tests for 5 stores
- 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).
2026-04-07 17:08:34 +08:00

320 lines
12 KiB
TypeScript

/**
* 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();
});
});