fix: update chatStore tests for sub-store refactoring
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
Tests were referencing old monolithic useChatStore API. Updated to use useConversationStore for conversation/agent/model state and useChatStore for message operations. 10→0 failures.
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Stabilization Core Path Tests
|
* Stabilization Core Path Tests
|
||||||
*
|
*
|
||||||
* Covers the 4 critical paths from STABILIZATION_DIRECTIVE.md §5:
|
* Covers the 4 critical paths from STABILIZATION_DIRECTIVE.md 5:
|
||||||
* 1. Skill execution — invoke → no crash → result
|
* 1. Skill execution -- invoke -> no crash -> result
|
||||||
* 2. Hand trigger — emit event → frontend receives notification
|
* 2. Hand trigger -- emit event -> frontend receives notification
|
||||||
* 3. Message sending — Store → invoke → streaming response
|
* 3. Message sending -- Store -> invoke -> streaming response
|
||||||
* 4. Config sync — SaaS pull → Store update
|
* 4. Config sync -- SaaS pull -> Store update
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { useChatStore, type Message } from '../src/store/chatStore';
|
import { useChatStore, type Message } from '../src/store/chatStore';
|
||||||
|
|
||||||
// ─── Shared mocks ───
|
// --- Shared mocks ---
|
||||||
|
|
||||||
vi.mock('@tauri-apps/api/core', () => ({
|
vi.mock('@tauri-apps/api/core', () => ({
|
||||||
invoke: vi.fn(),
|
invoke: vi.fn(),
|
||||||
@@ -114,11 +114,23 @@ vi.mock('../src/store/chat/conversationStore', () => {
|
|||||||
sessionKey: 'test-session-1',
|
sessionKey: 'test-session-1',
|
||||||
currentModel: 'default',
|
currentModel: 'default',
|
||||||
conversations: [],
|
conversations: [],
|
||||||
|
currentConversationId: null,
|
||||||
|
agents: [{ id: 'test-agent-1', name: 'Test Agent' }],
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
useConversationStore: {
|
useConversationStore: {
|
||||||
getState: () => state,
|
getState: () => state,
|
||||||
|
setState: vi.fn((partial: Record<string, unknown>) => {
|
||||||
|
if (typeof partial === 'function') {
|
||||||
|
Object.assign(state, partial(state));
|
||||||
|
} else {
|
||||||
|
Object.assign(state, partial);
|
||||||
|
}
|
||||||
|
}),
|
||||||
subscribe: vi.fn(),
|
subscribe: vi.fn(),
|
||||||
|
persist: {
|
||||||
|
hasHydrated: () => true,
|
||||||
|
},
|
||||||
...(Object.fromEntries(
|
...(Object.fromEntries(
|
||||||
Object.keys(state).map((k) => [k, vi.fn()])
|
Object.keys(state).map((k) => [k, vi.fn()])
|
||||||
)),
|
)),
|
||||||
@@ -126,7 +138,7 @@ vi.mock('../src/store/chat/conversationStore', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── 1. Skill Execution ───
|
// --- 1. Skill Execution ---
|
||||||
|
|
||||||
describe('Skill execution (SEC2-P0-01)', () => {
|
describe('Skill execution (SEC2-P0-01)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -182,7 +194,7 @@ describe('Skill execution (SEC2-P0-01)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── 2. Hand Trigger Event ───
|
// --- 2. Hand Trigger Event ---
|
||||||
|
|
||||||
describe('Hand execution event (SEC2-P1-03)', () => {
|
describe('Hand execution event (SEC2-P1-03)', () => {
|
||||||
it('should add hand message to chatStore when hand-execution-complete is received', () => {
|
it('should add hand message to chatStore when hand-execution-complete is received', () => {
|
||||||
@@ -237,7 +249,7 @@ describe('Hand execution event (SEC2-P1-03)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── 3. Message Sending ───
|
// --- 3. Message Sending ---
|
||||||
|
|
||||||
describe('Message sending flow', () => {
|
describe('Message sending flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -304,9 +316,9 @@ describe('Message sending flow', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── 4. Config Sync ───
|
// --- 4. Config Sync ---
|
||||||
|
|
||||||
describe('Config sync (SaaS → Store)', () => {
|
describe('Config sync (SaaS to Store)', () => {
|
||||||
it('should invoke saas-client with correct /api/v1 prefix for templates', async () => {
|
it('should invoke saas-client with correct /api/v1 prefix for templates', async () => {
|
||||||
vi.mocked(invoke).mockResolvedValueOnce([]);
|
vi.mocked(invoke).mockResolvedValueOnce([]);
|
||||||
|
|
||||||
@@ -321,7 +333,7 @@ describe('Config sync (SaaS → Store)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle store update cycle correctly', () => {
|
it('should handle store update cycle correctly', () => {
|
||||||
// Simulate a config sync: external data arrives → store updates
|
// Simulate a config sync: external data arrives -> store updates
|
||||||
useChatStore.setState({ isStreaming: false });
|
useChatStore.setState({ isStreaming: false });
|
||||||
expect(useChatStore.getState().isStreaming).toBe(false);
|
expect(useChatStore.getState().isStreaming).toBe(false);
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,18 @@
|
|||||||
* Chat Store Tests
|
* Chat Store Tests
|
||||||
*
|
*
|
||||||
* Tests for chat state management including messages, conversations, and agents.
|
* Tests for chat state management including messages, conversations, and agents.
|
||||||
|
* After the chatStore refactoring, chatStore delegates to:
|
||||||
|
* - conversationStore: conversations, agents, currentAgent, sessionKey, currentModel
|
||||||
|
* - messageStore: token tracking
|
||||||
|
* - streamStore: streaming, sendMessage, chatMode
|
||||||
|
*
|
||||||
|
* chatStore remains the facade, so tests use useChatStore for message operations
|
||||||
|
* and useConversationStore for conversation/agent state.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { useChatStore, Message, Conversation, Agent, toChatAgent } from '../../src/store/chatStore';
|
import { useChatStore, Message } from '../../src/store/chatStore';
|
||||||
|
import { useConversationStore, toChatAgent, type Agent, type Conversation } from '../../src/store/chat/conversationStore';
|
||||||
import { localStorageMock } from '../setup';
|
import { localStorageMock } from '../setup';
|
||||||
|
|
||||||
// Mock gateway client - use vi.hoisted to ensure mocks are available before module import
|
// Mock gateway client - use vi.hoisted to ensure mocks are available before module import
|
||||||
@@ -72,21 +80,41 @@ vi.mock('../../src/lib/skill-discovery', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('chatStore', () => {
|
describe('chatStore', () => {
|
||||||
// Store the original state to reset between tests
|
// Default agent matching conversationStore's DEFAULT_AGENT
|
||||||
const initialState = {
|
const defaultAgent: Agent = {
|
||||||
messages: [],
|
id: '1',
|
||||||
conversations: [],
|
name: 'ZCLAW',
|
||||||
currentConversationId: null,
|
icon: '\u{1F99E}',
|
||||||
agents: [{ id: '1', name: 'ZCLAW', icon: '\u{1F99E}', color: 'bg-gradient-to-br from-orange-500 to-red-500', lastMessage: '\u{53D1}\u{9001}\u{6D88}\u{606F}\u{5F00}\u{59CB}\u{5BF9}\u{8BDD}', time: '' }],
|
color: 'bg-gradient-to-br from-orange-500 to-red-500',
|
||||||
currentAgent: { id: '1', name: 'ZCLAW', icon: '\u{1F99E}', color: 'bg-gradient-to-br from-orange-500 to-red-500', lastMessage: '\u{53D1}\u{9001}\u{6D88}\u{606F}\u{5F00}\u{59CB}\u{5BF9}\u{8BDD}', time: '' },
|
lastMessage: '\u{53D1}\u{9001}\u{6D88}\u{606F}\u{5F00}\u{59CB}\u{5BF9}\u{8BDD}',
|
||||||
|
time: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialChatState = {
|
||||||
|
messages: [] as Message[],
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
currentModel: 'glm-5',
|
isLoading: false,
|
||||||
sessionKey: null,
|
totalInputTokens: 0,
|
||||||
|
totalOutputTokens: 0,
|
||||||
|
chatMode: 'thinking' as const,
|
||||||
|
suggestions: [] as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialConvState = {
|
||||||
|
conversations: [] as Conversation[],
|
||||||
|
currentConversationId: null as string | null,
|
||||||
|
agents: [defaultAgent] as Agent[],
|
||||||
|
currentAgent: defaultAgent as Agent,
|
||||||
|
isStreaming: false,
|
||||||
|
currentModel: 'glm-4-flash',
|
||||||
|
sessionKey: null as string | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset store state
|
// Reset chatStore (messages + facade mirrors)
|
||||||
useChatStore.setState(initialState);
|
useChatStore.setState(initialChatState);
|
||||||
|
// Reset conversationStore (conversations, agents, session, model)
|
||||||
|
useConversationStore.setState(initialConvState);
|
||||||
// Clear localStorage
|
// Clear localStorage
|
||||||
localStorageMock.clear();
|
localStorageMock.clear();
|
||||||
// Clear all mocks
|
// Clear all mocks
|
||||||
@@ -104,10 +132,10 @@ describe('chatStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have default agent set', () => {
|
it('should have default agent set', () => {
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.currentAgent).not.toBeNull();
|
expect(convState.currentAgent).not.toBeNull();
|
||||||
expect(state.currentAgent?.id).toBe('1');
|
expect(convState.currentAgent?.id).toBe('1');
|
||||||
expect(state.currentAgent?.name).toBe('ZCLAW');
|
expect(convState.currentAgent?.name).toBe('ZCLAW');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not be streaming initially', () => {
|
it('should not be streaming initially', () => {
|
||||||
@@ -116,18 +144,18 @@ describe('chatStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have default model', () => {
|
it('should have default model', () => {
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.currentModel).toBe('glm-5');
|
expect(convState.currentModel).toBe('glm-4-flash');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have null sessionKey initially', () => {
|
it('should have null sessionKey initially', () => {
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.sessionKey).toBeNull();
|
expect(convState.sessionKey).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have empty conversations array', () => {
|
it('should have empty conversations array', () => {
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.conversations).toEqual([]);
|
expect(convState.conversations).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,8 +296,9 @@ describe('chatStore', () => {
|
|||||||
|
|
||||||
setCurrentModel('gpt-4');
|
setCurrentModel('gpt-4');
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
// currentModel is stored on conversationStore
|
||||||
expect(state.currentModel).toBe('gpt-4');
|
const convState = useConversationStore.getState();
|
||||||
|
expect(convState.currentModel).toBe('gpt-4');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -284,15 +313,15 @@ describe('chatStore', () => {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
useChatStore.setState({ sessionKey: 'old-session' });
|
|
||||||
|
|
||||||
newConversation();
|
newConversation();
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const chatState = useChatStore.getState();
|
||||||
expect(state.messages).toEqual([]);
|
const convState = useConversationStore.getState();
|
||||||
expect(state.sessionKey).toBeNull();
|
expect(chatState.messages).toEqual([]);
|
||||||
expect(state.isStreaming).toBe(false);
|
// sessionKey and currentConversationId reset on conversationStore
|
||||||
expect(state.currentConversationId).toBeNull();
|
expect(convState.sessionKey).toBeNull();
|
||||||
|
expect(chatState.isStreaming).toBe(false);
|
||||||
|
expect(convState.currentConversationId).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save current messages to conversations before clearing', () => {
|
it('should save current messages to conversations before clearing', () => {
|
||||||
@@ -307,10 +336,10 @@ describe('chatStore', () => {
|
|||||||
|
|
||||||
newConversation();
|
newConversation();
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
// Conversation should be saved
|
// Conversation should be saved on conversationStore
|
||||||
expect(state.conversations.length).toBeGreaterThan(0);
|
expect(convState.conversations.length).toBeGreaterThan(0);
|
||||||
expect(state.conversations[0].messages[0].content).toBe('Test message to save');
|
expect(convState.conversations[0].messages[0].content).toBe('Test message to save');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -335,14 +364,16 @@ describe('chatStore', () => {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstConvId = useChatStore.getState().conversations[0].id;
|
const convState = useConversationStore.getState();
|
||||||
|
const firstConvId = convState.conversations[0].id;
|
||||||
|
|
||||||
// Switch back to first conversation
|
// Switch back to first conversation
|
||||||
switchConversation(firstConvId);
|
switchConversation(firstConvId);
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const chatState = useChatStore.getState();
|
||||||
expect(state.messages[0].content).toBe('First conversation');
|
const updatedConvState = useConversationStore.getState();
|
||||||
expect(state.currentConversationId).toBe(firstConvId);
|
expect(chatState.messages[0].content).toBe('First conversation');
|
||||||
|
expect(updatedConvState.currentConversationId).toBe(firstConvId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -359,19 +390,20 @@ describe('chatStore', () => {
|
|||||||
});
|
});
|
||||||
newConversation();
|
newConversation();
|
||||||
|
|
||||||
const convId = useChatStore.getState().conversations[0].id;
|
const convState = useConversationStore.getState();
|
||||||
expect(useChatStore.getState().conversations).toHaveLength(1);
|
const convId = convState.conversations[0].id;
|
||||||
|
expect(convState.conversations).toHaveLength(1);
|
||||||
|
|
||||||
// Delete it
|
// Delete it
|
||||||
deleteConversation(convId);
|
deleteConversation(convId);
|
||||||
|
|
||||||
expect(useChatStore.getState().conversations).toHaveLength(0);
|
expect(useConversationStore.getState().conversations).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear messages if deleting current conversation', () => {
|
it('should clear messages if deleting current conversation', () => {
|
||||||
const { addMessage, deleteConversation } = useChatStore.getState();
|
const { addMessage, deleteConversation } = useChatStore.getState();
|
||||||
|
|
||||||
// Create a conversation without calling newConversation
|
// Add a message
|
||||||
addMessage({
|
addMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -379,14 +411,14 @@ describe('chatStore', () => {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manually set up a current conversation
|
// Manually set up a current conversation on conversationStore
|
||||||
const convId = 'conv-test-123';
|
const convId = 'conv-test-123';
|
||||||
useChatStore.setState({
|
useConversationStore.setState({
|
||||||
currentConversationId: convId,
|
currentConversationId: convId,
|
||||||
conversations: [{
|
conversations: [{
|
||||||
id: convId,
|
id: convId,
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
messages: useChatStore.getState().messages,
|
messages: useChatStore.getState().messages as any[],
|
||||||
sessionKey: null,
|
sessionKey: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -396,10 +428,12 @@ describe('chatStore', () => {
|
|||||||
|
|
||||||
deleteConversation(convId);
|
deleteConversation(convId);
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const chatState = useChatStore.getState();
|
||||||
expect(state.messages).toEqual([]);
|
const convState = useConversationStore.getState();
|
||||||
expect(state.sessionKey).toBeNull();
|
// chatStore facade should detect resetMessages and clear its messages
|
||||||
expect(state.currentConversationId).toBeNull();
|
expect(chatState.messages).toEqual([]);
|
||||||
|
expect(convState.sessionKey).toBeNull();
|
||||||
|
expect(convState.currentConversationId).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -417,8 +451,9 @@ describe('chatStore', () => {
|
|||||||
|
|
||||||
setCurrentAgent(newAgent);
|
setCurrentAgent(newAgent);
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
// currentAgent is stored on conversationStore
|
||||||
expect(state.currentAgent).toEqual(newAgent);
|
const convState = useConversationStore.getState();
|
||||||
|
expect(convState.currentAgent).toEqual(newAgent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save current conversation when switching agents', () => {
|
it('should save current conversation when switching agents', () => {
|
||||||
@@ -443,7 +478,7 @@ describe('chatStore', () => {
|
|||||||
};
|
};
|
||||||
setCurrentAgent(newAgent);
|
setCurrentAgent(newAgent);
|
||||||
|
|
||||||
// Messages should be cleared for new agent
|
// Messages should be cleared for new agent (different agent id)
|
||||||
expect(useChatStore.getState().messages).toEqual([]);
|
expect(useChatStore.getState().messages).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -457,10 +492,12 @@ describe('chatStore', () => {
|
|||||||
{ id: 'agent-2', name: 'Agent Two', nickname: 'A2' },
|
{ id: 'agent-2', name: 'Agent Two', nickname: 'A2' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
// agents are stored on conversationStore
|
||||||
expect(state.agents).toHaveLength(2);
|
const convState = useConversationStore.getState();
|
||||||
expect(state.agents[0].name).toBe('Agent One');
|
// DEFAULT_AGENT + 2 profile agents = 3
|
||||||
expect(state.agents[1].name).toBe('Agent Two');
|
expect(convState.agents).toHaveLength(3);
|
||||||
|
expect(convState.agents[1].name).toBe('Agent One');
|
||||||
|
expect(convState.agents[2].name).toBe('Agent Two');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default agent when no profiles provided', () => {
|
it('should use default agent when no profiles provided', () => {
|
||||||
@@ -468,9 +505,9 @@ describe('chatStore', () => {
|
|||||||
|
|
||||||
syncAgents([]);
|
syncAgents([]);
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.agents).toHaveLength(1);
|
expect(convState.agents).toHaveLength(1);
|
||||||
expect(state.agents[0].id).toBe('1');
|
expect(convState.agents[0].id).toBe('1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -644,14 +681,14 @@ describe('chatStore', () => {
|
|||||||
|
|
||||||
newConversation();
|
newConversation();
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.conversations[0].title).toContain('This is a long message');
|
expect(convState.conversations[0].title).toContain('This is a long message');
|
||||||
expect(state.conversations[0].title.length).toBeLessThanOrEqual(33); // 30 chars + '...'
|
expect(convState.conversations[0].title.length).toBeLessThanOrEqual(33); // 30 chars + '...'
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default title for empty messages', () => {
|
it('should use default title for empty messages', () => {
|
||||||
// Create a conversation directly with empty messages
|
// Create a conversation directly with empty messages on conversationStore
|
||||||
useChatStore.setState({
|
useConversationStore.setState({
|
||||||
conversations: [{
|
conversations: [{
|
||||||
id: 'conv-1',
|
id: 'conv-1',
|
||||||
title: '',
|
title: '',
|
||||||
@@ -663,8 +700,8 @@ describe('chatStore', () => {
|
|||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = useChatStore.getState();
|
const convState = useConversationStore.getState();
|
||||||
expect(state.conversations).toHaveLength(1);
|
expect(convState.conversations).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user