eigent/test/unit/store/chatStore.test.ts
2025-10-16 19:27:14 +03:00

486 lines
15 KiB
TypeScript

/**
* ChatStore Unit Tests - Core Functionality
*
* Tests basic chatStore operations:
* - Task creation and removal
* - Status management
* - Token tracking
* - Message handling
*/
import { describe, it, expect, beforeEach, vi, MockedFunction } from 'vitest'
import { act, renderHook } from '@testing-library/react'
// Mock dependencies - moved to top before other imports
vi.mock('@/api/http', () => ({
fetchPost: vi.fn(),
fetchPut: vi.fn(),
getBaseURL: vi.fn(() => Promise.resolve('http://localhost:8000')),
proxyFetchPost: vi.fn(),
proxyFetchPut: vi.fn(),
proxyFetchGet: vi.fn(),
uploadFile: vi.fn(),
fetchDelete: vi.fn(),
}))
vi.mock('@microsoft/fetch-event-source', () => ({
fetchEventSource: vi.fn(),
}))
vi.mock('../../../src/store/authStore', () => ({
useAuthStore: {
token: null,
username: null,
email: null,
user_id: null,
appearance: 'light',
language: 'system',
isFirstLaunch: true,
modelType: 'cloud' as const,
cloud_model_type: 'gpt-4.1' as const,
initState: 'permissions' as const,
share_token: null,
workerListData: {},
},
getAuthStore: vi.fn(() => ({
token: null,
username: null,
email: null,
user_id: null,
appearance: 'light',
language: 'system',
isFirstLaunch: true,
modelType: 'cloud' as const,
cloud_model_type: 'gpt-4.1' as const,
initState: 'permissions' as const,
share_token: null,
workerListData: {},
})),
useWorkerList: vi.fn(() => [])
}))
import { useChatStore } from '../../../src/store/chatStore'
import { useProjectStore } from '../../../src/store/projectStore'
import { generateUniqueId } from '../../../src/lib'
// Mock electron IPC
(global as any).ipcRenderer = {
invoke: vi.fn((channel, ...args) => {
if (channel === 'get-system-language') return Promise.resolve('en')
if (channel === 'get-browser-port') return Promise.resolve(9222)
if (channel === 'get-env-path') return Promise.resolve('/path/to/env')
if (channel === 'mcp-list') return Promise.resolve({})
return Promise.resolve()
}),
}
describe('ChatStore - Core Functionality', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Task Creation', () => {
it('should create a task with unique ID', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId1 = result.current.getState().create()
const taskId2 = result.current.getState().create()
expect(taskId1).toBeDefined()
expect(taskId2).toBeDefined()
expect(taskId1).not.toBe(taskId2)
expect(result.current.getState().tasks[taskId1]).toBeDefined()
expect(result.current.getState().tasks[taskId2]).toBeDefined()
})
})
it('should create a task with custom ID', () => {
const { result } = renderHook(() => useChatStore())
const customId = 'custom-task-123'
act(() => {
const taskId = result.current.getState().create(customId)
expect(taskId).toBe(customId)
expect(result.current.getState().tasks[customId]).toBeDefined()
})
})
it('should initialize task with correct default state', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
const task = result.current.getState().tasks[taskId]
expect(task.status).toBe('pending')
expect(task.messages).toEqual([])
expect(task.tokens).toBe(0)
expect(task.isPending).toBe(false)
expect(task.hasWaitComfirm).toBe(false)
expect(task.progressValue).toBe(0)
expect(task.taskInfo).toEqual([])
expect(task.taskRunning).toEqual([])
expect(task.taskAssigning).toEqual([])
})
})
it('should set task as active when created', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
expect(result.current.getState().activeTaskId).toBe(taskId)
})
})
})
describe('Task Removal', () => {
it('should remove a task by ID', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
expect(result.current.getState().tasks[taskId]).toBeDefined()
result.current.getState().removeTask(taskId)
expect(result.current.getState().tasks[taskId]).toBeUndefined()
})
})
it('should handle removing non-existent task gracefully', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
// Should not throw
result.current.getState().removeTask('non-existent-id')
})
})
it('should clear all tasks and create new one', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId1 = result.current.getState().create()
const taskId2 = result.current.getState().create()
expect(Object.keys(result.current.getState().tasks)).toHaveLength(2)
result.current.getState().clearTasks()
const remainingTasks = Object.keys(result.current.getState().tasks)
expect(remainingTasks).toHaveLength(1)
expect(result.current.getState().activeTaskId).toBeDefined()
})
})
})
describe('Status Management', () => {
it('should update task status correctly', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().setStatus(taskId, 'running')
expect(result.current.getState().tasks[taskId].status).toBe('running')
result.current.getState().setStatus(taskId, 'finished')
expect(result.current.getState().tasks[taskId].status).toBe('finished')
result.current.getState().setStatus(taskId, 'pause')
expect(result.current.getState().tasks[taskId].status).toBe('pause')
})
})
it('should set pending state independently of status', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().setIsPending(taskId, true)
expect(result.current.getState().tasks[taskId].isPending).toBe(true)
expect(result.current.getState().tasks[taskId].status).toBe('pending')
result.current.getState().setStatus(taskId, 'running')
expect(result.current.getState().tasks[taskId].isPending).toBe(true)
expect(result.current.getState().tasks[taskId].status).toBe('running')
})
})
})
describe('Token Management', () => {
it('should accumulate tokens correctly', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().addTokens(taskId, 100)
expect(result.current.getState().getTokens(taskId)).toBe(100)
result.current.getState().addTokens(taskId, 50)
expect(result.current.getState().getTokens(taskId)).toBe(150)
result.current.getState().addTokens(taskId, 250)
expect(result.current.getState().getTokens(taskId)).toBe(400)
})
})
it('should handle negative token additions', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().addTokens(taskId, 100)
result.current.getState().addTokens(taskId, -50)
expect(result.current.getState().getTokens(taskId)).toBe(50)
})
})
it('should return 0 tokens for non-existent task', () => {
const { result } = renderHook(() => useChatStore())
expect(result.current.getState().getTokens('non-existent')).toBe(0)
})
it('should preserve tokens when creating new task with initial tokens', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId1 = result.current.getState().create()
result.current.getState().addTokens(taskId1, 500)
// Simulate new task in same project with accumulated tokens
const taskId2 = result.current.getState().create()
result.current.getState().addTokens(taskId2, 500) // Cumulative
expect(result.current.getState().getTokens(taskId2)).toBe(500)
})
})
})
describe('Message Management', () => {
it('should add messages to task', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().addMessages(taskId, {
id: generateUniqueId(),
role: 'user',
content: 'Hello, world!'
})
expect(result.current.getState().tasks[taskId].messages).toHaveLength(1)
expect(result.current.getState().tasks[taskId].messages[0].content).toBe('Hello, world!')
})
})
it('should maintain message order', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().addMessages(taskId, {
id: '1',
role: 'user',
content: 'First'
})
result.current.getState().addMessages(taskId, {
id: '2',
role: 'agent',
content: 'Second'
})
result.current.getState().addMessages(taskId, {
id: '3',
role: 'user',
content: 'Third'
})
const messages = result.current.getState().tasks[taskId].messages
expect(messages).toHaveLength(3)
expect(messages[0].content).toBe('First')
expect(messages[1].content).toBe('Second')
expect(messages[2].content).toBe('Third')
})
})
it('should get last user message', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().setActiveTaskId(taskId)
result.current.getState().addMessages(taskId, {
id: '1',
role: 'user',
content: 'First user message'
})
result.current.getState().addMessages(taskId, {
id: '2',
role: 'agent',
content: 'Agent response'
})
result.current.getState().addMessages(taskId, {
id: '3',
role: 'user',
content: 'Second user message'
})
const lastUserMessage = result.current.getState().getLastUserMessage()
expect(lastUserMessage?.content).toBe('Second user message')
})
})
it('should return null when no user messages exist', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().setActiveTaskId(taskId)
result.current.getState().addMessages(taskId, {
id: '1',
role: 'agent',
content: 'Agent message'
})
const lastUserMessage = result.current.getState().getLastUserMessage()
expect(lastUserMessage).toBeNull()
})
})
it('should set messages replacing existing ones', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().addMessages(taskId, {
id: '1',
role: 'user',
content: 'Original'
})
const newMessages = [
{ id: '2', role: 'user' as const, content: 'New 1' },
{ id: '3', role: 'agent' as const, content: 'New 2' }
]
result.current.getState().setMessages(taskId, newMessages)
expect(result.current.getState().tasks[taskId].messages).toHaveLength(2)
expect(result.current.getState().tasks[taskId].messages[0].content).toBe('New 1')
})
})
})
describe('Task Time Tracking', () => {
it('should track task time', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
const startTime = Date.now()
result.current.getState().setTaskTime(taskId, startTime)
expect(result.current.getState().tasks[taskId].taskTime).toBe(startTime)
})
})
it('should track elapsed time', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().setElapsed(taskId, 5000)
expect(result.current.getState().tasks[taskId].elapsed).toBe(5000)
})
})
it('should format task time correctly', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
// Test elapsed time formatting
result.current.getState().setTaskTime(taskId, 0)
result.current.getState().setElapsed(taskId, 3665000) // 1h 1m 5s
const formatted = result.current.getState().getFormattedTaskTime(taskId)
expect(formatted).toBe('01:01:05')
})
})
})
describe('Progress Tracking', () => {
it('should update progress value', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
result.current.getState().setProgressValue(taskId, 50)
expect(result.current.getState().tasks[taskId].progressValue).toBe(50)
result.current.getState().setProgressValue(taskId, 100)
expect(result.current.getState().tasks[taskId].progressValue).toBe(100)
})
})
it('should compute progress based on completed tasks', () => {
const { result } = renderHook(() => useChatStore())
act(() => {
const taskId = result.current.getState().create()
// Set up task structure
result.current.getState().setTaskRunning(taskId, [
{ id: '1', content: 'Task 1', status: 'completed' },
{ id: '2', content: 'Task 2', status: 'completed' },
{ id: '3', content: 'Task 3', status: 'running' },
{ id: '4', content: 'Task 4', status: 'waiting' },
] as any)
result.current.getState().computedProgressValue(taskId)
// 2 out of 4 = 50%
expect(result.current.getState().tasks[taskId].progressValue).toBe(50)
})
})
})
describe('Update Counter', () => {
it('should increment update count', () => {
const { result } = renderHook(() => useChatStore())
const initialCount = result.current.getState().updateCount
act(() => {
result.current.getState().setUpdateCount()
})
expect(result.current.getState().updateCount).toBe(initialCount + 1)
act(() => {
result.current.getState().setUpdateCount()
})
expect(result.current.getState().updateCount).toBe(initialCount + 2)
})
})
})