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).
320 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|