mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-14 16:42:47 +00:00
Merge branch 'test-multiturn-logic' into enhance-replay
This commit is contained in:
commit
da56183193
10 changed files with 3378 additions and 0 deletions
769
test/integration/chatStore/activeQueue.test.tsx
Normal file
769
test/integration/chatStore/activeQueue.test.tsx
Normal file
|
|
@ -0,0 +1,769 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { generateUniqueId } from '../../../src/lib'
|
||||
|
||||
// Import proxy mock to enable API mocking
|
||||
import '../../mocks/proxy.mock'
|
||||
// Also Mock authStore & sse
|
||||
import '../../mocks/authStore.mock'
|
||||
import '../../mocks/sse.mock'
|
||||
|
||||
// Import chat store to ensure it's available
|
||||
import '../../../src/store/chatStore'
|
||||
|
||||
import { useProjectStore } from '../../../src/store/projectStore'
|
||||
import useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter'
|
||||
import { mockFetchEventSource } from '../../mocks/sse.mock'
|
||||
|
||||
// Helper function for sequential SSE events
|
||||
const createSSESequence = (events: Array<{ event: any; delay: number }>) => {
|
||||
return async (onMessage: (data: any) => void) => {
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const { event, delay } = events[i]
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(`Sending SSE Event ${i + 1}:`, event.step);
|
||||
onMessage({
|
||||
data: JSON.stringify(event)
|
||||
})
|
||||
resolve()
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock electron IPC
|
||||
(global as any).ipcRenderer = {
|
||||
invoke: vi.fn((channel) => {
|
||||
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({})
|
||||
if (channel === 'get-file-list') return Promise.resolve([])
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock window.electronAPI
|
||||
Object.defineProperty(window, 'electronAPI', {
|
||||
value: {
|
||||
uploadLog: vi.fn().mockResolvedValue(undefined),
|
||||
// Add other electronAPI methods as needed
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
describe("Case 3: Add to the workforce queue", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
//Reset projectStore
|
||||
result.current.getAllProjects().forEach(project => {
|
||||
result.current.removeProject(project.id)
|
||||
})
|
||||
|
||||
//Create initial Project
|
||||
const projectId = result.current.createProject(
|
||||
'Queue Test Project',
|
||||
'Testing message queue functionality'
|
||||
)
|
||||
expect(projectId).toBeDefined()
|
||||
|
||||
// Get chatStore (automatically created)
|
||||
let chatStore = result.current.getActiveChatStore(projectId)!
|
||||
expect(chatStore).toBeDefined()
|
||||
const initiatorTaskId = chatStore.getState().activeTaskId!
|
||||
expect(initiatorTaskId).toBeDefined()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should queue messages when task is busy and process them after completion", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
let sseCallCount = 0
|
||||
let queuedTaskIds: string[] = []
|
||||
|
||||
// Mock SSE stream with controlled events - delay setup until after task IDs are available
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
sseCallCount++
|
||||
console.log(`SSE Call #${sseCallCount} initiated`)
|
||||
|
||||
if (options.onmessage) {
|
||||
// First send the immediate events (confirmed, to_sub_tasks, end)
|
||||
const immediateSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Build a calculator app' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a simple calculator',
|
||||
sub_tasks: [
|
||||
{ id: 'task-1', content: 'Create UI components', status: '' },
|
||||
{ id: 'task-2', content: 'Implement calculator logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Calculator Task Result ---\nCalculator app completed successfully!"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
await immediateSequence(options.onmessage)
|
||||
|
||||
// Wait for queuedTaskIds to be populated, then send queue-related events
|
||||
const checkForTaskIds = () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const pollForIds = () => {
|
||||
if (queuedTaskIds.length > 0) {
|
||||
console.log(`Found queued task IDs: ${queuedTaskIds}`)
|
||||
resolve()
|
||||
} else {
|
||||
setTimeout(pollForIds, 50) // Check every 50ms
|
||||
}
|
||||
}
|
||||
pollForIds()
|
||||
})
|
||||
}
|
||||
|
||||
await checkForTaskIds()
|
||||
|
||||
// Now send the queue-related events with actual task IDs
|
||||
const queueSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'new_task_state',
|
||||
data: {
|
||||
task_id: queuedTaskIds[0], // First queued task ID
|
||||
content: 'Build a calculator app 2',
|
||||
project_id: projectId
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'remove_task',
|
||||
data: {
|
||||
task_id: queuedTaskIds[0], // Remove first task from queue
|
||||
project_id: projectId
|
||||
}
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Build a calculator app 2' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a simple calculator 2',
|
||||
sub_tasks: [
|
||||
{ id: 'task-1', content: 'Create UI components 2', status: '' },
|
||||
{ id: 'task-2', content: 'Implement calculator logic 2', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Queue Result ---\nCalculator app completed successfully!"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
await queueSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Get initial state
|
||||
const { chatStore: initialChatStore, projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
const initiatorTaskId = initialChatStore.activeTaskId
|
||||
|
||||
// Verify initial queue is empty
|
||||
expect(projectStore.getProjectById(projectId)?.queuedMessages).toEqual([])
|
||||
|
||||
// Step 1: Start first task
|
||||
await act(async () => {
|
||||
const userMessage = 'Build a calculator app'
|
||||
await initialChatStore.startTask(initiatorTaskId, undefined, undefined, undefined, userMessage)
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Wait for task to start and reach 'to_sub_tasks' phase (task becomes busy)
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore, projectStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
// Task should have subtasks (making it busy)
|
||||
expect(task.summaryTask).toBe('Calculator App|Build a simple calculator')
|
||||
expect(task.taskInfo).toHaveLength(3)
|
||||
console.log("Task reached to_sub_tasks phase - now busy")
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// Step 2: Add messages to queue while task is busy
|
||||
await act(async () => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
|
||||
// Add first queued message
|
||||
const tempMessageContent1 = 'Build a todo app'
|
||||
const currentAttaches1: any[] = []
|
||||
const new_task_id_1 = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
tempMessageContent1,
|
||||
currentAttaches1
|
||||
)
|
||||
|
||||
// Add second queued message
|
||||
const tempMessageContent2 = 'Create a weather app'
|
||||
const currentAttaches2: any[] = []
|
||||
const new_task_id_2 = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
tempMessageContent2,
|
||||
currentAttaches2
|
||||
)
|
||||
|
||||
expect(new_task_id_1).toBeDefined()
|
||||
expect(new_task_id_2).toBeDefined()
|
||||
expect(new_task_id_1).not.toBe(new_task_id_2)
|
||||
|
||||
// Store task IDs for SSE events
|
||||
queuedTaskIds = [new_task_id_1!, new_task_id_2!]
|
||||
|
||||
console.log("Added messages to queue:", { new_task_id_1, new_task_id_2 })
|
||||
})
|
||||
|
||||
// Step 3: Verify messages are in queue
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
|
||||
expect(project?.queuedMessages).toHaveLength(2)
|
||||
expect(project?.queuedMessages?.[0].content).toBe('Build a todo app')
|
||||
expect(project?.queuedMessages?.[1].content).toBe('Create a weather app')
|
||||
|
||||
console.log("Queue verified with 2 messages")
|
||||
})
|
||||
|
||||
// Step 4: Wait for task completion
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
expect(task.status).toBe('finished')
|
||||
console.log("Main task completed")
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// Step 5: Wait for new_task_state event to process queue
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
//Get new appended chatStore
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
// Look for new_task_state in messages
|
||||
const hasNewTaskState = task.messages.some((m: any) => m.content === 'Build a calculator app 2')
|
||||
expect(hasNewTaskState).toBe(true)
|
||||
console.log("new_task_state event detected - new chat created")
|
||||
}, { timeout: 3000 })
|
||||
|
||||
// Step 6: Wait for remove_task event to clear queue
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
|
||||
// After remove_task event, first queued message should be removed, leaving 1 message
|
||||
expect(project?.queuedMessages).toHaveLength(1)
|
||||
expect(project?.queuedMessages?.[0].content).toBe('Create a weather app')
|
||||
|
||||
console.log("Queue processed - first message removed")
|
||||
}, { timeout: 4000 });
|
||||
|
||||
//Waitfor end sse
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: finalChatStore, projectStore: finalProjectStore } = result.current;
|
||||
const finalTaskId = finalChatStore.activeTaskId;
|
||||
const finalTask = finalChatStore.tasks[finalTaskId];
|
||||
expect(finalTask.status).toBe('finished');
|
||||
})
|
||||
|
||||
// Step 7: Verify final state
|
||||
const { chatStore: finalChatStore, projectStore: finalProjectStore } = result.current
|
||||
const finalProject = finalProjectStore.getProjectById(projectId)
|
||||
|
||||
// Queue should have 1 remaining message (the second one)
|
||||
expect(finalProject?.queuedMessages).toHaveLength(1)
|
||||
expect(finalProject?.queuedMessages?.[0].content).toBe('Create a weather app')
|
||||
|
||||
// Verify task completed successfully
|
||||
const finalTaskId = finalChatStore.activeTaskId
|
||||
const finalTask = finalChatStore.tasks[finalTaskId]
|
||||
expect(finalTask.status).toBe('finished')
|
||||
//Not to be because its a new chatStore
|
||||
expect(finalTask.summaryTask).not.toBe('Calculator App|Build a simple calculator')
|
||||
expect(finalTask.summaryTask).toBe('Calculator App|Build a simple calculator 2')
|
||||
|
||||
console.log("Test completed - queue management verified: one task processed, one remains")
|
||||
})
|
||||
|
||||
it("should handle multiple queue additions and removals correctly", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
|
||||
// Verify initial state
|
||||
expect(projectStore.getProjectById(projectId)?.queuedMessages).toEqual([])
|
||||
|
||||
await act(async () => {
|
||||
// Add multiple messages to queue
|
||||
const messages = [
|
||||
'Build a calculator',
|
||||
'Create a todo app',
|
||||
'Develop a weather app',
|
||||
'Make a chat application'
|
||||
]
|
||||
|
||||
const taskIds: string[] = []
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
const taskId = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
message,
|
||||
[]
|
||||
)
|
||||
taskIds.push(taskId)
|
||||
expect(taskId).toBeDefined()
|
||||
})
|
||||
|
||||
// Verify all messages are queued
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
expect(project?.queuedMessages).toHaveLength(4)
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
expect(project?.queuedMessages?.[index].content).toBe(message)
|
||||
expect(project?.queuedMessages?.[index].task_id).toBe(taskIds[index])
|
||||
})
|
||||
|
||||
// Remove middle message
|
||||
projectStore.removeQueuedMessage(projectId, taskIds[1])
|
||||
|
||||
// Verify removal
|
||||
const updatedProject = projectStore.getProjectById(projectId)
|
||||
expect(updatedProject?.queuedMessages).toHaveLength(3)
|
||||
expect(updatedProject?.queuedMessages?.map((m: any) => m.content)).toEqual([
|
||||
'Build a calculator',
|
||||
'Develop a weather app',
|
||||
'Make a chat application'
|
||||
])
|
||||
|
||||
// Remove first message
|
||||
projectStore.removeQueuedMessage(projectId, taskIds[0])
|
||||
|
||||
// Verify second removal
|
||||
const finalProject = projectStore.getProjectById(projectId)
|
||||
expect(finalProject?.queuedMessages).toHaveLength(2)
|
||||
expect(finalProject?.queuedMessages?.map((m: any) => m.content)).toEqual([
|
||||
'Develop a weather app',
|
||||
'Make a chat application'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it("should restore queued message when removal fails", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
|
||||
await act(async () => {
|
||||
// Add a message to queue
|
||||
const messageContent = 'Test message for restoration'
|
||||
const attachments: any[] = [{ fileName: 'test.txt', filePath: '/test/path' }]
|
||||
|
||||
const taskId = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
messageContent,
|
||||
attachments
|
||||
)
|
||||
|
||||
// Verify message is queued
|
||||
let project = projectStore.getProjectById(projectId)
|
||||
expect(project?.queuedMessages).toHaveLength(1)
|
||||
expect(project?.queuedMessages?.[0].content).toBe(messageContent)
|
||||
expect(project?.queuedMessages?.[0].attaches).toEqual(attachments)
|
||||
|
||||
// Store original message for comparison
|
||||
const originalMessage = project?.queuedMessages?.[0]
|
||||
|
||||
// Remove the message (this would normally trigger an API call)
|
||||
projectStore.removeQueuedMessage(projectId, taskId)
|
||||
|
||||
// Verify optimistic removal
|
||||
project = projectStore.getProjectById(projectId)
|
||||
expect(project?.queuedMessages).toHaveLength(0)
|
||||
|
||||
// Simulate restoration (as would happen on API failure)
|
||||
if (originalMessage) {
|
||||
projectStore.restoreQueuedMessage(projectId, {
|
||||
task_id: originalMessage.task_id,
|
||||
content: originalMessage.content,
|
||||
timestamp: originalMessage.timestamp,
|
||||
attaches: originalMessage.attaches
|
||||
})
|
||||
}
|
||||
|
||||
// Verify message is restored
|
||||
project = projectStore.getProjectById(projectId)
|
||||
expect(project?.queuedMessages).toHaveLength(1)
|
||||
expect(project?.queuedMessages?.[0].content).toBe(messageContent)
|
||||
expect(project?.queuedMessages?.[0].attaches).toEqual(attachments)
|
||||
expect(project?.queuedMessages?.[0].task_id).toBe(taskId)
|
||||
})
|
||||
})
|
||||
|
||||
it("should maintain queue order and timestamps correctly", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
|
||||
await act(async () => {
|
||||
const messages = ['First message', 'Second message', 'Third message']
|
||||
const taskIds: string[] = []
|
||||
const timestamps: number[] = []
|
||||
|
||||
// Add messages with small delays to ensure different timestamps
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
|
||||
const taskId = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
messages[i],
|
||||
[]
|
||||
)
|
||||
taskIds.push(taskId)
|
||||
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
const addedMessage = project?.queuedMessages?.find((m: any) => m.task_id === taskId)
|
||||
if (addedMessage) {
|
||||
timestamps.push(addedMessage.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify order and timestamps
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
expect(project?.queuedMessages).toHaveLength(3)
|
||||
|
||||
project?.queuedMessages?.forEach((message: any, index: number) => {
|
||||
expect(message.content).toBe(messages[index])
|
||||
expect(message.task_id).toBe(taskIds[index])
|
||||
expect(message.timestamp).toBe(timestamps[index])
|
||||
|
||||
// Verify timestamps are in ascending order
|
||||
if (index > 0) {
|
||||
expect(message.timestamp).toBeGreaterThanOrEqual(timestamps[index - 1])
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle confirmed -> subtasks -> end -> new_task_state -> remove_task sequence with queue processing", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
let comprehensiveQueuedTaskIds: string[] = []
|
||||
|
||||
// Get initial state
|
||||
const { chatStore: initialChatStore, projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
const initiatorTaskId = initialChatStore.activeTaskId
|
||||
|
||||
// Verify initial queue is empty
|
||||
expect(projectStore.getProjectById(projectId)?.queuedMessages).toEqual([])
|
||||
|
||||
// Mock SSE stream with controlled events - delay queue-related events until task IDs are available
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
if (options.onmessage) {
|
||||
// First send the immediate events (confirmed, to_sub_tasks, end)
|
||||
const immediateSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Build a calculator app' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a comprehensive calculator',
|
||||
sub_tasks: [
|
||||
{ id: 'calc-ui', content: 'Create calculator UI', status: '' },
|
||||
{ id: 'calc-logic', content: 'Implement calculation logic', status: '' },
|
||||
{ id: 'calc-tests', content: 'Write unit tests', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Task Completed Successfully ---\nCalculator app development finished!"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
await immediateSequence(options.onmessage)
|
||||
|
||||
// Wait for comprehensiveQueuedTaskIds to be populated, then send queue-related events
|
||||
const checkForTaskIds = () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const pollForIds = () => {
|
||||
if (comprehensiveQueuedTaskIds.length > 0) {
|
||||
console.log(`Found comprehensive queued task IDs: ${comprehensiveQueuedTaskIds}`)
|
||||
resolve()
|
||||
} else {
|
||||
setTimeout(pollForIds, 50) // Check every 50ms
|
||||
}
|
||||
}
|
||||
pollForIds()
|
||||
})
|
||||
}
|
||||
|
||||
await checkForTaskIds()
|
||||
|
||||
// Now send the queue-related events with actual task IDs
|
||||
const queueSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: {
|
||||
task_id: comprehensiveQueuedTaskIds[0], // First queued task ID
|
||||
question: 'Build a todo application',
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'new_task_state',
|
||||
data: {
|
||||
task_id: comprehensiveQueuedTaskIds[0], // First queued task ID
|
||||
content: 'Build a todo application',
|
||||
project_id: projectId
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'remove_task',
|
||||
data: {
|
||||
task_id: comprehensiveQueuedTaskIds[0], // Remove first task from queue
|
||||
project_id: projectId
|
||||
}
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Task Completed Successfully ---\nCalculator app development finished! 2"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
await queueSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Step 1: Start the main task
|
||||
await act(async () => {
|
||||
const userMessage = 'Build a calculator app'
|
||||
await initialChatStore.startTask(initiatorTaskId, undefined, undefined, undefined, userMessage)
|
||||
rerender()
|
||||
})
|
||||
// Step 2: Wait for confirmed event
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
// Check for confirmed step
|
||||
const hasContent = task.messages.some((m: any) => m.content === 'Build a calculator app')
|
||||
expect(hasContent).toBe(true)
|
||||
console.log("✓ Confirmed event received")
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Step 3: Wait for subtasks event
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
// Task should have subtasks
|
||||
expect(task.summaryTask).toBe('Calculator App|Build a comprehensive calculator')
|
||||
expect(task.taskInfo).toHaveLength(4) // main task + 3 subtasks
|
||||
expect(task.taskRunning).toHaveLength(4)
|
||||
console.log("✓ Subtasks created")
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// Step 4: Add messages to queue while task is in subtasks phase
|
||||
await act(async () => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
|
||||
// Add queued messages
|
||||
const tempMessageContent1 = 'Build a todo application'
|
||||
const currentAttaches1: any[] = [{ fileName: 'requirements.txt', filePath: '/path/to/req.txt' }]
|
||||
const new_task_id_1 = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
tempMessageContent1,
|
||||
currentAttaches1
|
||||
)
|
||||
|
||||
const tempMessageContent2 = 'Create a weather dashboard'
|
||||
const currentAttaches2: any[] = []
|
||||
const new_task_id_2 = projectStore.addQueuedMessage(
|
||||
projectId,
|
||||
tempMessageContent2,
|
||||
currentAttaches2
|
||||
)
|
||||
|
||||
expect(new_task_id_1).toBeDefined()
|
||||
expect(new_task_id_2).toBeDefined()
|
||||
|
||||
// Store task IDs for SSE events
|
||||
comprehensiveQueuedTaskIds = [new_task_id_1!, new_task_id_2!]
|
||||
|
||||
console.log("✓ Messages added to queue during subtasks phase")
|
||||
})
|
||||
|
||||
// Step 5: Verify messages are properly queued
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
|
||||
expect(project?.queuedMessages).toHaveLength(2)
|
||||
expect(project?.queuedMessages?.[0].content).toBe('Build a todo application')
|
||||
expect(project?.queuedMessages?.[1].content).toBe('Create a weather dashboard')
|
||||
expect(project?.queuedMessages?.[0].attaches).toHaveLength(1)
|
||||
expect(project?.queuedMessages?.[1].attaches).toHaveLength(0)
|
||||
console.log("✓ Queue contains expected messages with attachments")
|
||||
})
|
||||
|
||||
// Step 6: Wait for task completion
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
expect(task.status).toBe('finished')
|
||||
console.log("✓ Main task completed")
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// Step 7: Wait for new_task_state event (new chat creation)
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore.activeTaskId
|
||||
const task = chatStore.tasks[taskId]
|
||||
|
||||
// Look for new_task_state event
|
||||
const hasNewTaskState = task.messages.some((m: any) => m.content === 'Build a todo application')
|
||||
expect(hasNewTaskState).toBe(true)
|
||||
console.log("✓ new_task_state event received - new chat created")
|
||||
}, { timeout: 3000 })
|
||||
|
||||
// Step 8: Wait for remove_task event to process queue
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const project = projectStore.getProjectById(projectId)
|
||||
|
||||
// After remove_task event, first queued message should be removed, leaving 1 message
|
||||
expect(project?.queuedMessages).toHaveLength(1)
|
||||
expect(project?.queuedMessages?.[0].content).toBe('Create a weather dashboard')
|
||||
|
||||
console.log("✓ Queue processed - first message removed")
|
||||
}, { timeout: 4000 })
|
||||
|
||||
//Waitfor end sse
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: finalChatStore, projectStore: finalProjectStore } = result.current;
|
||||
const finalTaskId = finalChatStore.activeTaskId;
|
||||
const finalTask = finalChatStore.tasks[finalTaskId];
|
||||
expect(finalTask.status).toBe('finished');
|
||||
})
|
||||
|
||||
// Step 9: Final verification
|
||||
const { chatStore: finalChatStore, projectStore: finalProjectStore } = result.current
|
||||
const finalProject = finalProjectStore.getProjectById(projectId)
|
||||
const finalTaskId = finalChatStore.activeTaskId
|
||||
const finalTask = finalChatStore.tasks[finalTaskId]
|
||||
|
||||
// Queue should have 1 remaining message (the second one)
|
||||
expect(finalProject?.queuedMessages).toHaveLength(1)
|
||||
expect(finalProject?.queuedMessages?.[0].content).toBe('Create a weather dashboard')
|
||||
|
||||
expect(finalTask.status).toBe('finished')
|
||||
//This time lets not add sub_task sse
|
||||
expect(finalTask.summaryTask).not.toBe('Calculator App|Build a comprehensive calculator')
|
||||
|
||||
//Get previous chatStore
|
||||
const [initial, first, last] = finalProjectStore.getAllChatStores(finalProjectStore.activeProjectId);
|
||||
const originalTaskId = first.chatStore.getState().activeTaskId;
|
||||
const originalFinalTask = first.chatStore.getState().tasks[originalTaskId];
|
||||
expect(originalFinalTask.summaryTask).toBe('Calculator App|Build a comprehensive calculator')
|
||||
expect(originalFinalTask.taskInfo).toHaveLength(4)
|
||||
|
||||
// Verify all subtasks are properly marked as skipped after completion
|
||||
originalFinalTask.taskRunning.forEach((task: any) => {
|
||||
expect(task.status).toBe("skipped")
|
||||
})
|
||||
originalFinalTask.taskInfo.forEach((task: any) => {
|
||||
expect(task.status).toBe("skipped")
|
||||
})
|
||||
|
||||
console.log("✓ Complete test sequence verified: confirmed → subtasks → end → new_task_state → remove_task → one task processed, one remains")
|
||||
})
|
||||
})
|
||||
637
test/integration/chatStore/deadWorkforce.test.tsx
Normal file
637
test/integration/chatStore/deadWorkforce.test.tsx
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { generateUniqueId } from '../../../src/lib'
|
||||
|
||||
// Import proxy mock to enable API mocking
|
||||
import '../../mocks/proxy.mock'
|
||||
// Also Mock authStore & sse
|
||||
import '../../mocks/authStore.mock'
|
||||
import '../../mocks/sse.mock'
|
||||
|
||||
// Import chat store to ensure it's available
|
||||
import '../../../src/store/chatStore'
|
||||
|
||||
import { useProjectStore } from '../../../src/store/projectStore'
|
||||
import useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter'
|
||||
import { mockFetchEventSource } from '../../mocks/sse.mock'
|
||||
|
||||
// Helper function for sequential SSE events
|
||||
const createSSESequence = (events: Array<{ event: any; delay: number }>) => {
|
||||
return async (onMessage: (data: any) => void) => {
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const { event, delay } = events[i]
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(`Sending SSE Event ${i + 1}:`, event.step);
|
||||
onMessage({
|
||||
data: JSON.stringify(event)
|
||||
})
|
||||
resolve()
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock electron IPC
|
||||
(global as any).ipcRenderer = {
|
||||
invoke: vi.fn((channel) => {
|
||||
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({})
|
||||
if (channel === 'get-file-list') return Promise.resolve([])
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock window.electronAPI
|
||||
Object.defineProperty(window, 'electronAPI', {
|
||||
value: {
|
||||
uploadLog: vi.fn().mockResolvedValue(undefined),
|
||||
// Add other electronAPI methods as needed
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
|
||||
describe('Integration Test: Case 2 - same session new chat', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
//Reset projectStore
|
||||
result.current.getAllProjects().forEach(project => {
|
||||
result.current.removeProject(project.id)
|
||||
})
|
||||
|
||||
//Create initial Project
|
||||
const projectId = result.current.createProject(
|
||||
'Complete Journey Test',
|
||||
'Testing full flow'
|
||||
)
|
||||
expect(projectId).toBeDefined()
|
||||
|
||||
// 2. Get chatStore (automatically created)
|
||||
let chatStore = result.current.getActiveChatStore(projectId)!
|
||||
expect(chatStore).toBeDefined()
|
||||
const initiatorTaskId = chatStore.getState().activeTaskId!
|
||||
expect(initiatorTaskId).toBeDefined()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Sequential startTask appends chatStores to same project", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
// Setup the events sequence
|
||||
const eventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a simple calculator',
|
||||
sub_tasks: [
|
||||
{ id: 'task-1', content: 'Create UI components', status: '' },
|
||||
{ id: 'task-2', content: 'Implement calculator logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Subtask 1760647257350-6021.1 Result ---\nI'm doing well, thank you for asking! How are you today?"
|
||||
},
|
||||
delay: 400
|
||||
}
|
||||
])
|
||||
|
||||
// Mock SSE stream with controlled events
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
if (options.onmessage) {
|
||||
await eventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
// Complete Flow Test
|
||||
const {chatStore, projectStore} = result.current
|
||||
const initiatorTaskId = chatStore.activeTaskId;
|
||||
|
||||
//User Message to send
|
||||
const userMessage = 'Build a calculator app';
|
||||
|
||||
// 6. Start task
|
||||
// NOTE: startTask creates a NEW chatStore and switches to it, the old chatStore is no longer active
|
||||
await chatStore.startTask(initiatorTaskId, undefined, undefined, undefined, userMessage)
|
||||
|
||||
//Rerender to get the latest chatStore
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Test 1: Check initial task creation and first SSE event (to_sub_tasks)
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore: newChatStore, projectStore} = result.current;
|
||||
expect(newChatStore).toBeDefined()
|
||||
|
||||
let taskId = newChatStore?.activeTaskId!
|
||||
const task = newChatStore?.tasks[taskId]
|
||||
expect(taskId).toBeDefined()
|
||||
|
||||
if(task) {
|
||||
expect(task.hasMessages).toBe(true)
|
||||
expect(task.messages[0].content).toBe('Build a calculator app')
|
||||
expect(task.status).toBe('pending')
|
||||
}
|
||||
|
||||
const updatedTask = newChatStore?.tasks[taskId]
|
||||
expect(updatedTask?.summaryTask).toBe('Calculator App|Build a simple calculator')
|
||||
expect(updatedTask?.taskInfo).toHaveLength(3)
|
||||
expect(updatedTask?.taskRunning).toHaveLength(3)
|
||||
|
||||
//Two chatStores - first initial
|
||||
expect(projectStore.getAllChatStores(projectStore.activeProjectId)).toHaveLength(2)
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Test 2: Check progress event has been processed
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore: newChatStore} = result.current;
|
||||
let taskId = newChatStore?.activeTaskId!
|
||||
const task = newChatStore?.tasks[taskId]
|
||||
|
||||
// Check if progress message exists in messages or status updates
|
||||
// Adjust this based on how your app handles progress events
|
||||
expect(task).toBeDefined()
|
||||
console.log("Progress test - task status:", task?.status);
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// Test 3: Rerender untill status is "finished"
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore: newChatStore} = result.current;
|
||||
let taskId = newChatStore?.activeTaskId!
|
||||
const task = newChatStore?.tasks[taskId]
|
||||
|
||||
if(task) {
|
||||
// Check if task is completed or has final result
|
||||
// Adjust these assertions based on your app's behavior
|
||||
console.log("End test - task status:", task?.status);
|
||||
console.log("End test - task messages:", task?.messages?.length);
|
||||
}
|
||||
expect(task.status).toBe("finished")
|
||||
console.log(task);
|
||||
}, { timeout: 2000 });
|
||||
|
||||
//Before starting new chatStore
|
||||
const { chatStore, projectStore } = result.current;
|
||||
expect(Object.keys(chatStore.tasks)).toHaveLength(1);
|
||||
//Initial ChatStore + appendedOne
|
||||
expect(projectStore.getAllChatStores(projectStore.activeProjectId)).toHaveLength(2)
|
||||
//Make all tasks are skipped after end
|
||||
chatStore.tasks[chatStore.activeTaskId].taskRunning.forEach((task:any) => {
|
||||
expect(task.status).toBe("skipped")
|
||||
})
|
||||
chatStore.tasks[chatStore.activeTaskId].taskInfo.forEach((task:any) => {
|
||||
expect(task.status).toBe("skipped")
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Test: Start second chat session with different events
|
||||
await act(async () => {
|
||||
rerender()
|
||||
const {chatStore, projectStore} = result.current
|
||||
const initiatorTaskId = chatStore.activeTaskId;
|
||||
|
||||
// Setup different events for second session
|
||||
const secondEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
"step": "confirmed",
|
||||
"data": {"question": "how are you?"}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Todo App|Build a todo application',
|
||||
sub_tasks: [
|
||||
{ id: 'task-3', content: 'Design todo interface', status: '' },
|
||||
{ id: 'task-4', content: 'Implement todo logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Second Task Result ---\nTodo app planning completed!"
|
||||
},
|
||||
delay: 300
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Subtask 1760647257350-6021.1 Result ---\nI'm doing well, thank you for asking! How are you today?"
|
||||
},
|
||||
delay: 400
|
||||
}
|
||||
])
|
||||
|
||||
// Update the mock for the second call
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
if (options.onmessage) {
|
||||
await secondEventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
const userMessage = 'Build a todo app';
|
||||
await chatStore.startTask(initiatorTaskId, undefined, undefined, undefined, userMessage)
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Test the second session results
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore: newChatStore, projectStore} = result.current;
|
||||
let taskId = newChatStore?.activeTaskId!
|
||||
const task = newChatStore?.tasks[taskId]
|
||||
|
||||
if(task) {
|
||||
expect(task.messages[0].content).toBe('Build a todo app')
|
||||
// Check if the new summary task is set correctly
|
||||
expect(task.summaryTask).toBe('Todo App|Build a todo application')
|
||||
}
|
||||
|
||||
// Should now have 3 chat stores (initial + 2 task sessions)
|
||||
expect(projectStore.getAllChatStores(projectStore.activeProjectId)).toHaveLength(3)
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// CHECK POST End State of Run 2
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore: newChatStore} = result.current;
|
||||
let taskId = newChatStore?.activeTaskId!
|
||||
const task = newChatStore?.tasks[taskId]
|
||||
|
||||
if(task) {
|
||||
// Check if task is completed or has final result
|
||||
// Adjust these assertions based on your app's behavior
|
||||
console.log("End test 2 - task status:", task?.status);
|
||||
console.log("End test 2 - task messages:", task?.messages?.length);
|
||||
}
|
||||
expect(task.status).toBe("finished")
|
||||
console.log(task);
|
||||
}, { timeout: 2000 });
|
||||
|
||||
//Before starting new chatStore
|
||||
const { chatStore:secondChatStore } = result.current;
|
||||
expect(Object.keys(secondChatStore.tasks)).toHaveLength(1);
|
||||
//Initial ChatStore + appendedOne
|
||||
expect(projectStore.getAllChatStores(projectStore.activeProjectId)).toHaveLength(3)
|
||||
//Make all tasks are skipped after end
|
||||
secondChatStore.tasks[secondChatStore.activeTaskId].taskRunning.forEach((task:any) => {
|
||||
expect(task.status).toBe("skipped")
|
||||
})
|
||||
secondChatStore.tasks[secondChatStore.activeTaskId].taskInfo.forEach((task:any) => {
|
||||
expect(task.status).toBe("skipped")
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle individual SSE events with precise timing", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
let messageCallback: ((data: any) => void) | null = null
|
||||
|
||||
// Mock SSE to capture the callback for manual event triggering
|
||||
mockFetchEventSource.mockImplementation((url: string, options: any) => {
|
||||
messageCallback = options.onmessage
|
||||
// Don't send any events automatically
|
||||
})
|
||||
|
||||
// Start the task
|
||||
await act(async () => {
|
||||
const {chatStore} = result.current
|
||||
const initiatorTaskId = chatStore.activeTaskId;
|
||||
await chatStore.startTask(initiatorTaskId, undefined, undefined, undefined, 'Test manual events')
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Manually send first event and test result
|
||||
await act(async () => {
|
||||
messageCallback?.({
|
||||
data: JSON.stringify({
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Manual Test|Testing manual event control',
|
||||
sub_tasks: [{ id: 'manual-1', content: 'Manual task 1', status: '' }],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Test first event result
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore} = result.current;
|
||||
let taskId = chatStore?.activeTaskId!
|
||||
const task = chatStore?.tasks[taskId]
|
||||
expect(task?.summaryTask).toBe('Manual Test|Testing manual event control')
|
||||
})
|
||||
|
||||
// Send second event and test result
|
||||
await act(async () => {
|
||||
messageCallback?.({
|
||||
data: JSON.stringify({
|
||||
step: 'progress',
|
||||
data: 'Manual progress update'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Test second event result
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore} = result.current;
|
||||
let taskId = chatStore?.activeTaskId!
|
||||
const task = chatStore?.tasks[taskId]
|
||||
// Add your specific assertions for progress events here
|
||||
expect(task).toBeDefined()
|
||||
})
|
||||
|
||||
// Send final event
|
||||
await act(async () => {
|
||||
messageCallback?.({
|
||||
data: JSON.stringify({
|
||||
step: 'end',
|
||||
data: 'Manual test completed successfully'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Test final state
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore} = result.current;
|
||||
let taskId = chatStore?.activeTaskId!
|
||||
const task = chatStore?.tasks[taskId]
|
||||
// Add your specific assertions for end state here
|
||||
expect(task).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
//TODO: Don't let new startTask untill newChatStore appended
|
||||
it("Parallel startTask calls with separate chatStores (startTask -> wait for append -> startTask)", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
let sseCallCount = 0
|
||||
let firstTaskChatStore: any = null
|
||||
let secondTaskChatStore: any = null
|
||||
|
||||
// Setup SSE events for the first task
|
||||
const firstTaskEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'First Task|Build a calculator app',
|
||||
sub_tasks: [
|
||||
{ id: 'first-1', content: 'Create calculator UI', status: '' },
|
||||
{ id: 'first-2', content: 'Implement calc logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- First Task Result ---\nCalculator app completed successfully!"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
|
||||
// Setup SSE events for the second task
|
||||
const secondTaskEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Second Task|Build a todo app',
|
||||
sub_tasks: [
|
||||
{ id: 'second-1', content: 'Create todo UI', status: '' },
|
||||
{ id: 'second-2', content: 'Implement todo logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Second Task Result ---\nTodo app completed successfully!"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
|
||||
// Mock SSE to handle sequential calls with different events
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
sseCallCount++
|
||||
console.log(`SSE Call #${sseCallCount} initiated`)
|
||||
|
||||
if (sseCallCount === 1) {
|
||||
// First task gets first event sequence
|
||||
console.log('Processing first task events')
|
||||
if (options.onmessage) {
|
||||
await firstTaskEventSequence(options.onmessage)
|
||||
}
|
||||
} else if (sseCallCount === 2) {
|
||||
// Second task gets second event sequence
|
||||
console.log('Processing second task events')
|
||||
if (options.onmessage) {
|
||||
await secondTaskEventSequence(options.onmessage)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Get initial chatStore reference
|
||||
const { chatStore: initialChatStore, projectStore } = result.current
|
||||
const initialChatStoreRef = initialChatStore
|
||||
const initiatorTaskId = initialChatStore.activeTaskId
|
||||
|
||||
// Step 1: Start first task
|
||||
await act(async () => {
|
||||
const userMessage1 = 'Build a calculator app'
|
||||
await initialChatStore.startTask(initiatorTaskId, undefined, undefined, undefined, userMessage1)
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Verify first task started and chatStore was created
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore, projectStore } = result.current
|
||||
|
||||
// Should have 2 chatStores: initial + first task
|
||||
const allChatStores = projectStore.getAllChatStores(projectStore.activeProjectId)
|
||||
expect(allChatStores).toHaveLength(2)
|
||||
|
||||
// Current chatStore should be different from initial (new one created)
|
||||
expect(currentChatStore).not.toBe(initialChatStoreRef)
|
||||
firstTaskChatStore = currentChatStore
|
||||
|
||||
// Verify first task details
|
||||
const activeTaskId = currentChatStore.activeTaskId
|
||||
const activeTask = currentChatStore.tasks[activeTaskId]
|
||||
expect(activeTask).toBeDefined()
|
||||
expect(activeTask.hasMessages).toBe(true)
|
||||
expect(activeTask.messages[0].content).toBe('Build a calculator app')
|
||||
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Wait for first task SSE events to process (summary_task)
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore } = result.current
|
||||
const activeTaskId = currentChatStore.activeTaskId
|
||||
const activeTask = currentChatStore.tasks[activeTaskId]
|
||||
|
||||
// Check that SSE events have been processed
|
||||
expect(activeTask.summaryTask).toBe('First Task|Build a calculator app')
|
||||
expect(activeTask.taskInfo).toHaveLength(3) // main task + 2 subtasks
|
||||
expect(activeTask.taskRunning).toHaveLength(3)
|
||||
|
||||
console.log("First task SSE events processed")
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// Wait for first task to complete
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore } = result.current
|
||||
const activeTaskId = currentChatStore.activeTaskId
|
||||
const activeTask = currentChatStore.tasks[activeTaskId]
|
||||
|
||||
expect(activeTask.status).toBe("finished")
|
||||
|
||||
console.log("First task completed")
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// Step 2: Wait for project append to complete before starting second task
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
|
||||
// Ensure the project has been properly updated with the appended first chatStore
|
||||
const allChatStores = projectStore.getAllChatStores(projectStore.activeProjectId)
|
||||
expect(allChatStores).toHaveLength(2)
|
||||
|
||||
// Verify the active chatStore is properly set and ready for next task
|
||||
const activeChatStore = projectStore.getActiveChatStore(projectStore.activeProjectId)
|
||||
expect(activeChatStore).toBeDefined()
|
||||
expect(activeChatStore.getState()?.activeTaskId).toBeDefined()
|
||||
|
||||
console.log("Project append completed, ready for second task")
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Step 3: Start second task on the same chatStore
|
||||
await act(async () => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore } = result.current
|
||||
const currentInitiatorTaskId = currentChatStore.activeTaskId
|
||||
|
||||
const userMessage2 = 'Build a todo app'
|
||||
await currentChatStore.startTask(currentInitiatorTaskId, undefined, undefined, undefined, userMessage2)
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Verify second task started and new chatStore was created
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore, projectStore } = result.current
|
||||
|
||||
// Should now have 3 chatStores: initial + first task + second task
|
||||
const allChatStores = projectStore.getAllChatStores(projectStore.activeProjectId)
|
||||
expect(allChatStores).toHaveLength(3)
|
||||
|
||||
// Current chatStore should be different from first task chatStore
|
||||
expect(currentChatStore).not.toBe(firstTaskChatStore)
|
||||
secondTaskChatStore = currentChatStore
|
||||
|
||||
// Verify second task details
|
||||
const activeTaskId = currentChatStore.activeTaskId
|
||||
const activeTask = currentChatStore.tasks[activeTaskId]
|
||||
expect(activeTask).toBeDefined()
|
||||
expect(activeTask.hasMessages).toBe(true)
|
||||
expect(activeTask.messages[0].content).toBe('Build a todo app')
|
||||
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Wait for second task SSE events to process (summary_task)
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore } = result.current
|
||||
const activeTaskId = currentChatStore.activeTaskId
|
||||
const activeTask = currentChatStore.tasks[activeTaskId]
|
||||
|
||||
// Check that SSE events have been processed
|
||||
expect(activeTask.summaryTask).toBe('Second Task|Build a todo app')
|
||||
expect(activeTask.taskInfo).toHaveLength(3) // main task + 2 subtasks
|
||||
expect(activeTask.taskRunning).toHaveLength(3)
|
||||
|
||||
console.log("Second task SSE events processed")
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// Wait for second task to complete
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: currentChatStore } = result.current
|
||||
const activeTaskId = currentChatStore.activeTaskId
|
||||
const activeTask = currentChatStore.tasks[activeTaskId]
|
||||
|
||||
expect(activeTask.status).toBe("finished")
|
||||
|
||||
console.log("Second task completed")
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// Final verification: Both chatStores should have separate states
|
||||
const { projectStore: finalProjectStore } = result.current
|
||||
const allFinalChatStores = finalProjectStore.getAllChatStores(finalProjectStore.activeProjectId)
|
||||
|
||||
// Verify we have 3 separate chatStores with their own states
|
||||
expect(allFinalChatStores).toHaveLength(3)
|
||||
expect(sseCallCount).toBe(2) // Two separate SSE calls
|
||||
|
||||
// Verify each chatStore has its own task with different content
|
||||
expect(firstTaskChatStore).not.toBe(secondTaskChatStore)
|
||||
|
||||
// Get the current state of chatStores from the project store to verify final states
|
||||
const [initialChatStoreFromProject, firstChatStoreFromProject, secondChatStoreFromProject] = allFinalChatStores
|
||||
|
||||
// Verify first chatStore state (should be the second in the array after initial)
|
||||
const firstTaskId = firstChatStoreFromProject.chatStore.getState().activeTaskId
|
||||
const firstTask = firstChatStoreFromProject.chatStore.getState().tasks[firstTaskId]
|
||||
expect(firstTask.messages[0].content).toBe('Build a calculator app')
|
||||
expect(firstTask.summaryTask).toBe('First Task|Build a calculator app')
|
||||
|
||||
// Verify second chatStore state (should be the third in the array)
|
||||
const secondTaskId = secondChatStoreFromProject.chatStore.getState().activeTaskId
|
||||
const secondTask = secondChatStoreFromProject.chatStore.getState().tasks[secondTaskId]
|
||||
expect(secondTask.messages[0].content).toBe('Build a todo app')
|
||||
expect(secondTask.summaryTask).toBe('Second Task|Build a todo app')
|
||||
|
||||
console.log('Sequential startTask test with separate chatStores completed successfully')
|
||||
})
|
||||
})
|
||||
372
test/integration/chatStore/newProject.test.tsx
Normal file
372
test/integration/chatStore/newProject.test.tsx
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
/**
|
||||
* Integration Test: Case 1 - New Project
|
||||
*
|
||||
* Tests the complete flow of creating a new project and sending the first message.
|
||||
*
|
||||
* Flow:
|
||||
* 1. User creates a new project with initial message
|
||||
* 2. System automatically creates initial chatStore
|
||||
* 3. Task starts executing
|
||||
*
|
||||
* This is the most common user journey and serves as the foundation for all other cases.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { generateUniqueId } from '../../../src/lib'
|
||||
|
||||
// Import proxy mock to enable API mocking
|
||||
import '../../mocks/proxy.mock'
|
||||
// Also Mock authStore & sse
|
||||
import '../../mocks/authStore.mock'
|
||||
import '../../mocks/sse.mock'
|
||||
|
||||
// Import chat store to ensure it's available
|
||||
import '../../../src/store/chatStore'
|
||||
|
||||
import { useProjectStore } from '../../../src/store/projectStore'
|
||||
import { mockFetchEventSource } from '../../mocks/sse.mock'
|
||||
|
||||
// Mock electron IPC
|
||||
(global as any).ipcRenderer = {
|
||||
invoke: vi.fn((channel) => {
|
||||
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({})
|
||||
if (channel === 'get-file-list') return Promise.resolve([])
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
describe('Integration Test: Case 1 - New Project', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
//Reset projectStore
|
||||
result.current.getAllProjects().forEach(project => {
|
||||
result.current.removeProject(project.id)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should create project with initial chatStore and task', async () => {
|
||||
const { result, rerender } = renderHook(() => useProjectStore())
|
||||
|
||||
await act(async () => {
|
||||
// Step 1: Create new project
|
||||
const projectId = result.current.createProject(
|
||||
'My First Project',
|
||||
'A test project'
|
||||
)
|
||||
|
||||
// Force a re-render to ensure state is updated
|
||||
rerender()
|
||||
|
||||
// Debug: Log the store state immediately after creation
|
||||
console.log('Created projectId:', projectId)
|
||||
|
||||
// Try to get project before asserting
|
||||
const debugProject = result.current.getProjectById(projectId)
|
||||
console.log('Retrieved project:', debugProject)
|
||||
|
||||
// Verify project created
|
||||
expect(projectId).toBeDefined()
|
||||
|
||||
// First check: activeProjectId should be set
|
||||
expect(debugProject?.id).toBe(projectId)
|
||||
|
||||
const project = result.current.getProjectById(projectId)
|
||||
console.log('Retrieved project again:', project)
|
||||
expect(project).toBeDefined()
|
||||
expect(project?.name).toBe('My First Project')
|
||||
expect(project?.description).toBe('A test project')
|
||||
})
|
||||
})
|
||||
|
||||
it('should automatically create initial chatStore in new project', () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
act(() => {
|
||||
const projectId = result.current.createProject('Test Project')
|
||||
|
||||
// Step 2: Verify chatStore created automatically
|
||||
const chatStore = result.current.getActiveChatStore(projectId)
|
||||
expect(chatStore).toBeDefined()
|
||||
|
||||
// Verify initial task exists
|
||||
const chatState = chatStore!.getState()
|
||||
expect(chatState.activeTaskId).toBeDefined()
|
||||
expect(chatState.tasks[chatState.activeTaskId!]).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should have correct initial task state', () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
act(() => {
|
||||
const projectId = result.current.createProject('Test Project')
|
||||
const chatStore = result.current.getActiveChatStore(projectId)!
|
||||
const chatState = chatStore.getState()
|
||||
const taskId = chatState.activeTaskId!
|
||||
const task = chatState.tasks[taskId]
|
||||
|
||||
// Verify task initial state
|
||||
expect(task.status).toBe('pending')
|
||||
expect(task.messages).toEqual([])
|
||||
expect(task.tokens).toBe(0)
|
||||
expect(task.isPending).toBe(false)
|
||||
expect(task.hasMessages).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should add user message to task', () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
act(() => {
|
||||
const projectId = result.current.createProject('Test Project')
|
||||
const chatStore = result.current.getActiveChatStore(projectId)!
|
||||
const taskId = chatStore.getState().activeTaskId!
|
||||
|
||||
// Step 3: User sends message
|
||||
const userMessage = {
|
||||
id: generateUniqueId(),
|
||||
role: 'user' as const,
|
||||
content: 'Create a todo app with React',
|
||||
attaches: [],
|
||||
}
|
||||
|
||||
chatStore.getState().addMessages(taskId, userMessage)
|
||||
chatStore.getState().setHasMessages(taskId, true)
|
||||
|
||||
// Verify message added
|
||||
const task = chatStore.getState().tasks[taskId]
|
||||
expect(task.messages).toHaveLength(1)
|
||||
expect(task.messages[0].content).toBe('Create a todo app with React')
|
||||
expect(task.hasMessages).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should create historyId after starting task', async () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
await act(async () => {
|
||||
const projectId = result.current.createProject('Test Project')
|
||||
const chatStore = result.current.getActiveChatStore(projectId)!
|
||||
const taskId = chatStore.getState().activeTaskId!
|
||||
|
||||
// Add message
|
||||
chatStore.getState().addMessages(taskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'user',
|
||||
content: 'Test message',
|
||||
})
|
||||
|
||||
// Mock SSE to immediately close (simulating startTask)
|
||||
mockFetchEventSource.mockImplementation((url: string, options: any) => {
|
||||
// Call onopen
|
||||
if (options.onopen) {
|
||||
options.onopen({ ok: true, status: 200 })
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
// Step 4: Start task
|
||||
await chatStore.getState().startTask(taskId)
|
||||
|
||||
// Wait for historyId to be set
|
||||
await waitFor(() => {
|
||||
const historyId = result.current.getHistoryId(projectId)
|
||||
expect(historyId).toBeDefined()
|
||||
expect(historyId).toMatch(/^history-/)
|
||||
}, { timeout: 2000 })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle complete user journey from project creation to task start', async () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
await act(async () => {
|
||||
// Complete Flow Test
|
||||
|
||||
// 1. Create project
|
||||
const projectId = result.current.createProject(
|
||||
'Complete Journey Test',
|
||||
'Testing full flow'
|
||||
)
|
||||
expect(projectId).toBeDefined()
|
||||
|
||||
// 2. Get chatStore (automatically created)
|
||||
const chatStore = result.current.getActiveChatStore(projectId)!
|
||||
expect(chatStore).toBeDefined()
|
||||
|
||||
const initiatorTaskId = chatStore.getState().activeTaskId!
|
||||
expect(initiatorTaskId).toBeDefined()
|
||||
|
||||
// 3. Set user message
|
||||
const userMessage = 'Build a calculator app';
|
||||
|
||||
// 4. Verify task ready to start
|
||||
const initialTask = chatStore.getState().tasks[initiatorTaskId]
|
||||
|
||||
|
||||
// 5. Mock SSE stream with to_sub_tasks event
|
||||
mockFetchEventSource.mockImplementation((url: string, options: any) => {
|
||||
setTimeout(() => {
|
||||
console.log("Sending to_sub_tasks SSE Event");
|
||||
// Simulate to_sub_tasks event
|
||||
if (options.onmessage) {
|
||||
options.onmessage({
|
||||
data: JSON.stringify({
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a simple calculator',
|
||||
sub_tasks: [
|
||||
{ id: 'task-1', content: 'Create UI components', status: '' },
|
||||
{ id: 'task-2', content: 'Implement calculator logic', status: '' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// 6. Start task
|
||||
// NOTE: startTask creates a NEW chatStore and switches to it, the old chatStore is no longer active
|
||||
await chatStore.getState().startTask(initiatorTaskId, undefined, undefined, undefined, userMessage)
|
||||
|
||||
// IMPORTANT: Get the NEW active chatStore after startTask creates it
|
||||
const newChatStore = result.current.getActiveChatStore()
|
||||
expect(newChatStore).toBeDefined()
|
||||
expect(newChatStore).not.toBe(chatStore) // Should be a different instance
|
||||
|
||||
let taskId = newChatStore?.getState().activeTaskId!
|
||||
const task = newChatStore?.getState().tasks[taskId]
|
||||
expect(taskId).toBeDefined()
|
||||
if(task) {
|
||||
expect(task.hasMessages).toBe(true)
|
||||
expect(task.messages[0].content).toBe('Build a calculator app')
|
||||
expect(task.status).toBe('pending')
|
||||
}
|
||||
|
||||
// 7. Wait for task breakdown
|
||||
await waitFor(() => {
|
||||
const updatedTask = newChatStore?.getState().tasks[taskId]
|
||||
expect(updatedTask?.summaryTask).toBe('Calculator App|Build a simple calculator')
|
||||
//Bcz of newTaskInfo { id: '', content: '', status: '' } we have 3 items
|
||||
expect(updatedTask?.taskInfo).toHaveLength(3)
|
||||
expect(updatedTask?.taskRunning).toHaveLength(3)
|
||||
}, { timeout: 2000 })
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create new project if empty project exists (optimization)', () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
act(() => {
|
||||
// Create first empty project
|
||||
const projectId1 = result.current.createProject('First Project')
|
||||
|
||||
// Before adding any messages, create another project
|
||||
// Should reuse the empty project
|
||||
const projectId2 = result.current.createProject('Second Project')
|
||||
|
||||
// Should reuse the same project ID
|
||||
expect(projectId2).toBe(projectId1)
|
||||
|
||||
// Project should be updated with new name
|
||||
const project = result.current.getProjectById(projectId2)
|
||||
expect(project?.name).toBe('Second Project')
|
||||
})
|
||||
})
|
||||
|
||||
it('should create new project if existing project has messages', () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
act(() => {
|
||||
// Create first project
|
||||
const projectId1 = result.current.createProject('First Project')
|
||||
|
||||
// Add a message (making it non-empty)
|
||||
const chatStore = result.current.getActiveChatStore(projectId1)!
|
||||
const taskId = chatStore.getState().activeTaskId!
|
||||
chatStore.getState().addMessages(taskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'user',
|
||||
content: 'Test message',
|
||||
})
|
||||
|
||||
// Now create second project
|
||||
const projectId2 = result.current.createProject('Second Project')
|
||||
|
||||
// Should create new project
|
||||
expect(projectId2).not.toBe(projectId1)
|
||||
expect(result.current.getAllProjects()).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle project creation with minimal data', async () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
await act(async () => {
|
||||
const projectId = result.current.createProject('Minimal Project')
|
||||
|
||||
// Wait a tick to ensure all state updates are complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
const project = result.current.getProjectById(projectId)
|
||||
expect(project?.name).toBe('Minimal Project')
|
||||
expect(project?.description).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty message gracefully', async () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
await act(async () => {
|
||||
const projectId = result.current.createProject('Test Project')
|
||||
|
||||
// Wait a tick for project creation to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
const chatStore = result.current.getActiveChatStore(projectId)!
|
||||
const taskId = chatStore.getState().activeTaskId!
|
||||
|
||||
// Add empty message
|
||||
chatStore.getState().addMessages(taskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'user',
|
||||
content: '',
|
||||
})
|
||||
|
||||
// Wait for message update to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
const task = chatStore.getState().tasks[taskId]
|
||||
expect(task.messages).toHaveLength(1)
|
||||
expect(task.messages[0].content).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle rapid project creation', async () => {
|
||||
const { result } = renderHook(() => useProjectStore())
|
||||
|
||||
await act(async () => {
|
||||
const projectIds = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
projectIds.push(result.current.createProject(`Project ${i}`))
|
||||
// Add small delay between each creation to ensure proper state updates
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
// Only first should be created, rest reuse until messages added
|
||||
expect(new Set(projectIds).size).toBeLessThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
526
test/integration/chatStore/replayComplete.test.tsx
Normal file
526
test/integration/chatStore/replayComplete.test.tsx
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { generateUniqueId } from '../../../src/lib'
|
||||
|
||||
// Import proxy mock to enable API mocking
|
||||
import '../../mocks/proxy.mock'
|
||||
// Also Mock authStore & sse
|
||||
import '../../mocks/authStore.mock'
|
||||
import '../../mocks/sse.mock'
|
||||
|
||||
// Import chat store to ensure it's available
|
||||
import '../../../src/store/chatStore'
|
||||
|
||||
import { useProjectStore } from '../../../src/store/projectStore'
|
||||
import useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter'
|
||||
import { mockFetchEventSource } from '../../mocks/sse.mock'
|
||||
import { replayProject } from '../../../src/lib'
|
||||
|
||||
// Helper function for sequential SSE events
|
||||
const createSSESequence = (events: Array<{ event: any; delay: number }>) => {
|
||||
return async (onMessage: (data: any) => void) => {
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const { event, delay } = events[i]
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(`Sending SSE Event ${i + 1}:`, event.step);
|
||||
onMessage({
|
||||
data: JSON.stringify(event)
|
||||
})
|
||||
resolve()
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock navigate function
|
||||
const mockNavigate = vi.fn() as any
|
||||
|
||||
// Mock electron IPC
|
||||
(global as any).ipcRenderer = {
|
||||
invoke: vi.fn((channel) => {
|
||||
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({})
|
||||
if (channel === 'get-file-list') return Promise.resolve([])
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock window.electronAPI
|
||||
Object.defineProperty(window, 'electronAPI', {
|
||||
value: {
|
||||
uploadLog: vi.fn().mockResolvedValue(undefined),
|
||||
// Add other electronAPI methods as needed
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
describe('Integration Test: Replay Functionality', () => {
|
||||
let initialProjectId: string
|
||||
let initialTaskId: string
|
||||
let projectStoreResult: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockNavigate.mockClear();
|
||||
|
||||
projectStoreResult = renderHook(() => useProjectStore());
|
||||
//Reset projectStore
|
||||
projectStoreResult.result.current.getAllProjects().forEach((project: any) => {
|
||||
projectStoreResult.result.current.removeProject(project.id)
|
||||
})
|
||||
|
||||
//Create initial Project for testing
|
||||
initialProjectId = projectStoreResult.result.current.createProject(
|
||||
'Original Project',
|
||||
'Testing replay functionality'
|
||||
)
|
||||
expect(initialProjectId).toBeDefined()
|
||||
|
||||
// Get chatStore (automatically created)
|
||||
const chatStore = projectStoreResult.result.current.getActiveChatStore(initialProjectId)!
|
||||
expect(chatStore).toBeDefined()
|
||||
initialTaskId = chatStore.getState().activeTaskId!
|
||||
expect(initialTaskId).toBeDefined()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should create replay project with correct taskId == projectId", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
// Setup replay events sequence
|
||||
const replayEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Build a calculator app' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a simple calculator',
|
||||
sub_tasks: [
|
||||
{ id: 'replay-task-1', content: 'Create UI components', status: '' },
|
||||
{ id: 'replay-task-2', content: 'Implement calculator logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Replay Task Result ---\nCalculator app replay completed!"
|
||||
},
|
||||
delay: 300
|
||||
}
|
||||
])
|
||||
|
||||
// Mock SSE for replay
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
console.log('SSE URL called:', url)
|
||||
if (url.includes('/api/chat/steps/playback/') && options.onmessage) {
|
||||
await replayEventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Run replayProject
|
||||
await act(async () => {
|
||||
await replayProject(
|
||||
result.current.projectStore,
|
||||
mockNavigate,
|
||||
generateUniqueId(), //Gotten from api
|
||||
'Build a calculator app',
|
||||
'test-history-id'
|
||||
)
|
||||
})
|
||||
|
||||
// Verify replay project was created
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {projectStore} = result.current;
|
||||
const projects = projectStore.getAllProjects()
|
||||
|
||||
// Should have original project + replay project
|
||||
expect(projects).toHaveLength(2)
|
||||
|
||||
// Find the replay project
|
||||
const replayProject = projects.find((p:any) => p.name.includes('Replay Project'))
|
||||
expect(replayProject).toBeDefined()
|
||||
expect(replayProject?.name).toBe('Replay Project Build a calculator app')
|
||||
|
||||
// Test critical requirement: taskId should equal projectId for replay
|
||||
const replayChatStores = projectStore.getAllChatStores(replayProject!.id)
|
||||
//Initial one is empty one - TODO: Reuse the empty one (even if projectid isgiven)
|
||||
expect(replayChatStores).toHaveLength(2)
|
||||
|
||||
const replayChatStore = replayChatStores[1].chatStore
|
||||
const replayTaskId = replayChatStore.getState().activeTaskId
|
||||
|
||||
// The main test: taskId should equal the projectId passed to replayProject
|
||||
// In this case we passed generateUniqueId() as the projectId
|
||||
expect(replayTaskId).toBeDefined()
|
||||
expect(replayTaskId).not.toBe(initialProjectId) // Should be different from initial project
|
||||
|
||||
// Verify the replay task has correct properties
|
||||
const replayTask = replayChatStore.getState().tasks[replayTaskId]
|
||||
expect(replayTask).toBeDefined()
|
||||
expect(replayTask.type).toBe('replay')
|
||||
expect(replayTask.messages[0].content).toBe('Build a calculator app')
|
||||
|
||||
console.log('Replay Project ID:', replayProject!.id)
|
||||
console.log('Replay Task ID:', replayTaskId)
|
||||
console.log('Original Project ID:', initialProjectId)
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// Verify navigation was called
|
||||
expect(mockNavigate).toHaveBeenCalledWith({ pathname: "/" })
|
||||
})
|
||||
|
||||
it("should not append chatStore during replay (appendingChatStore logic)", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const projectStoreResult = renderHook(() => useProjectStore())
|
||||
|
||||
// Setup replay events with multiple steps to test appendingChatStore logic
|
||||
const replayEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Build a todo app' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Todo App|Build a todo application',
|
||||
sub_tasks: [
|
||||
{ id: 'todo-1', content: 'Design interface', status: '' },
|
||||
{ id: 'todo-2', content: 'Implement logic', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 200
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'progress',
|
||||
data: 'Processing todo app requirements...'
|
||||
},
|
||||
delay: 300
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Todo App Replay Result ---\nTodo app replay finished!"
|
||||
},
|
||||
delay: 400
|
||||
}
|
||||
])
|
||||
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
if (url.includes('/api/chat/steps/playback/') && options.onmessage) {
|
||||
await replayEventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Get initial project count
|
||||
const initialProjectCount = projectStoreResult.result.current.getAllProjects().length
|
||||
|
||||
// Run replay
|
||||
await act(async () => {
|
||||
await replayProject(
|
||||
projectStoreResult.result.current,
|
||||
mockNavigate,
|
||||
generateUniqueId(),
|
||||
'Build a todo app',
|
||||
'test-history-id-2'
|
||||
)
|
||||
})
|
||||
|
||||
// Wait for replay to complete
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const projects = projectStore.getAllProjects()
|
||||
// We should have original project + replay project (so +1)
|
||||
expect(projects).toHaveLength(initialProjectCount + 1)
|
||||
|
||||
const replayProject = projects.find((p: any) => p.name.includes('Replay Project Build a todo app'))
|
||||
expect(replayProject).toBeDefined()
|
||||
|
||||
// Critical test: Should have exactly ONE chatStore in replay project
|
||||
// This tests that appendingChatStore logic prevented additional chatStores
|
||||
const replayChatStores = projectStore.getAllChatStores(replayProject!.id)
|
||||
expect(replayChatStores).toHaveLength(2)
|
||||
|
||||
// Verify the single chatStore has the replay task
|
||||
const replayChatStore = replayChatStores[1].chatStore
|
||||
const activeTaskId = replayChatStore.getState().activeTaskId
|
||||
const task = activeTaskId ? replayChatStore.getState().tasks[activeTaskId] : null
|
||||
expect(task).toBeDefined()
|
||||
expect(task?.summaryTask).toBe('Todo App|Build a todo application')
|
||||
|
||||
console.log('Replay ChatStore count:', replayChatStores.length)
|
||||
console.log('Should be exactly 1 (no appending during replay)')
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
|
||||
it("should handle startTask on same project after replay completes", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const projectStoreResult = renderHook(() => useProjectStore())
|
||||
|
||||
// Step 1: Complete a replay first
|
||||
const replayEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Initial replay task' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Initial Replay Completed ---"
|
||||
},
|
||||
delay: 200
|
||||
}
|
||||
])
|
||||
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
if (url.includes('/api/chat/steps/playback/') && options.onmessage) {
|
||||
await replayEventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Run initial replay
|
||||
await act(async () => {
|
||||
await replayProject(
|
||||
projectStoreResult.result.current,
|
||||
mockNavigate,
|
||||
generateUniqueId(),
|
||||
'Initial replay task',
|
||||
'replay-history-id'
|
||||
)
|
||||
})
|
||||
|
||||
// Wait for replay to complete
|
||||
await waitFor(() => {
|
||||
const projects = projectStoreResult.result.current.getAllProjects()
|
||||
const replayProj = projects.find(p => p.name.includes('Replay Project'))
|
||||
expect(replayProj).toBeDefined()
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// Step 2: Setup new SSE events for post-replay startTask
|
||||
const postReplayEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Post Replay Task|New task after replay',
|
||||
sub_tasks: [
|
||||
{ id: 'post-1', content: 'New task component', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Post Replay Task Completed ---"
|
||||
},
|
||||
delay: 200
|
||||
}
|
||||
])
|
||||
|
||||
// Update mock for post-replay events
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
if (!url.includes('/api/chat/steps/playback/') && options.onmessage) {
|
||||
await postReplayEventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Step 3: Call startTask on replay project after replay completes
|
||||
await act(async () => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
|
||||
// Should be connected to the replay project now
|
||||
expect(chatStore).toBeDefined()
|
||||
|
||||
const currentTaskId = chatStore.activeTaskId
|
||||
expect(currentTaskId).toBeDefined()
|
||||
|
||||
// Start a new task on the replay project
|
||||
await chatStore.startTask(
|
||||
currentTaskId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'New task after replay completion'
|
||||
)
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Step 4: Verify new chatStore was created for post-replay task
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { chatStore: newChatStore, projectStore } = result.current
|
||||
|
||||
// Should have a new chatStore for the post-replay task
|
||||
expect(newChatStore).toBeDefined()
|
||||
|
||||
const activeTaskId = newChatStore.activeTaskId
|
||||
const activeTask = newChatStore.tasks[activeTaskId]
|
||||
|
||||
expect(activeTask).toBeDefined()
|
||||
expect(activeTask.messages[0].content).toBe('New task after replay completion')
|
||||
expect(activeTask.summaryTask).toBe('Post Replay Task|New task after replay')
|
||||
|
||||
// Verify we now have 2 chatStores in the replay project (replay + post-replay task)
|
||||
const allChatStores = projectStore.getAllChatStores(projectStore.activeProjectId)
|
||||
// Expected: on createProject + original replay chatStore + new post-replay chatStore = 3
|
||||
expect(allChatStores).toHaveLength(3)
|
||||
|
||||
console.log('Post-replay chatStore count:', allChatStores.length)
|
||||
console.log('Successfully created new chatStore after replay')
|
||||
}, { timeout: 2000 })
|
||||
})
|
||||
|
||||
it("should handle parallel startTask during replay (separate chatStores)", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const projectStoreResult = renderHook(() => useProjectStore())
|
||||
|
||||
// Setup automatic SSE for both replay and parallel tasks
|
||||
const replayEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Long running replay task' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Replay Task Completed ---"
|
||||
},
|
||||
delay: 500 // Longer delay to allow parallel task to start
|
||||
}
|
||||
])
|
||||
|
||||
const parallelEventSequence = createSSESequence([
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Parallel Task|Running alongside replay',
|
||||
sub_tasks: [
|
||||
{ id: 'parallel-1', content: 'Parallel component', status: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: "end",
|
||||
data: "--- Parallel Task Completed ---"
|
||||
},
|
||||
delay: 200
|
||||
}
|
||||
])
|
||||
|
||||
// Mock SSE to handle both replay and parallel tasks automatically
|
||||
mockFetchEventSource.mockImplementation(async (url: string, options: any) => {
|
||||
console.log('Mock SSE called with URL:', url)
|
||||
if (url.includes('/api/chat/steps/playback/') && options.onmessage) {
|
||||
// This is replay SSE
|
||||
console.log('Processing replay events')
|
||||
await replayEventSequence(options.onmessage)
|
||||
} else if (options.onmessage) {
|
||||
// This is parallel startTask SSE
|
||||
console.log('Processing parallel task events')
|
||||
await parallelEventSequence(options.onmessage)
|
||||
}
|
||||
})
|
||||
|
||||
// Step 1: Start replay
|
||||
await act(async () => {
|
||||
await replayProject(
|
||||
projectStoreResult.result.current,
|
||||
mockNavigate,
|
||||
generateUniqueId(),
|
||||
'Long running replay task',
|
||||
'long-replay-history'
|
||||
)
|
||||
})
|
||||
|
||||
// Verify replay started
|
||||
await waitFor(() => {
|
||||
const projects = projectStoreResult.result.current.getAllProjects()
|
||||
const replayProj = projects.find(p => p.name.includes('Replay Project'))
|
||||
expect(replayProj).toBeDefined()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Step 2: While replay is running, start parallel task on same project
|
||||
await act(async () => {
|
||||
rerender()
|
||||
const { chatStore } = result.current
|
||||
|
||||
expect(chatStore).toBeDefined()
|
||||
const currentTaskId = chatStore.activeTaskId
|
||||
|
||||
// Start parallel task
|
||||
await chatStore.startTask(
|
||||
currentTaskId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'Parallel task during replay'
|
||||
)
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Step 3: Verify both tasks completed independently
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const { projectStore } = result.current
|
||||
const allChatStores = projectStore.getAllChatStores(projectStore.activeProjectId)
|
||||
|
||||
// Should have exactly 2 chatStores: onCreate + replay + parallel
|
||||
expect(allChatStores).toHaveLength(3)
|
||||
|
||||
// Get both chatStores and verify they have different content
|
||||
const chatStore1 = allChatStores[1].chatStore
|
||||
const chatStore2 = allChatStores[2].chatStore
|
||||
|
||||
const task1 = chatStore1.getState().tasks[chatStore1.getState().activeTaskId]
|
||||
const task2 = chatStore2.getState().tasks[chatStore2.getState().activeTaskId]
|
||||
|
||||
expect(task1).toBeDefined()
|
||||
expect(task2).toBeDefined()
|
||||
|
||||
// Verify they have different messages
|
||||
const contents = [task1.messages[0].content, task2.messages[0].content]
|
||||
expect(contents).toContain('Long running replay task')
|
||||
expect(contents).toContain('Parallel task during replay')
|
||||
|
||||
console.log('Parallel startTask during replay test completed successfully')
|
||||
console.log('Both tasks ran independently with separate chatStores')
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
})
|
||||
479
test/integration/components/ChatBox.integration.test.tsx
Normal file
479
test/integration/components/ChatBox.integration.test.tsx
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { render, screen, waitFor, act, renderHook } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ChatBox from '../../../src/components/ChatBox/index'
|
||||
import { generateUniqueId } from '../../../src/lib'
|
||||
|
||||
// Import proxy mock to enable API mocking
|
||||
import '../../mocks/proxy.mock'
|
||||
// Also Mock authStore & sse
|
||||
import '../../mocks/authStore.mock'
|
||||
import '../../mocks/sse.mock'
|
||||
|
||||
// Import chat store to ensure it's available
|
||||
import '../../../src/store/chatStore'
|
||||
|
||||
import { useProjectStore } from '../../../src/store/projectStore'
|
||||
import useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter'
|
||||
import { mockFetchEventSource } from '../../mocks/sse.mock'
|
||||
|
||||
// Mock electron IPC
|
||||
(global as any).ipcRenderer = {
|
||||
invoke: vi.fn((channel) => {
|
||||
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({})
|
||||
if (channel === 'get-file-list') return Promise.resolve([])
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock window.electronAPI
|
||||
Object.defineProperty(window, 'electronAPI', {
|
||||
value: {
|
||||
uploadLog: vi.fn().mockResolvedValue(undefined),
|
||||
// Add other electronAPI methods as needed
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
)
|
||||
|
||||
describe('ChatBox Integration Tests - Different ChatStore Configurations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
//Reset projectStore
|
||||
result.current.getAllProjects().forEach(project => {
|
||||
result.current.removeProject(project.id)
|
||||
})
|
||||
|
||||
//Create initial Project
|
||||
const projectId = result.current.createProject(
|
||||
'ChatBox Test Project',
|
||||
'Testing ChatBox UI functionality'
|
||||
)
|
||||
expect(projectId).toBeDefined()
|
||||
|
||||
// Get chatStore (automatically created)
|
||||
let chatStore = result.current.getActiveChatStore(projectId)!
|
||||
expect(chatStore).toBeDefined()
|
||||
const initiatorTaskId = chatStore.getState().activeTaskId!
|
||||
expect(initiatorTaskId).toBeDefined()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Task States and UI Rendering', () => {
|
||||
it('should render welcome screen when no messages exist', () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
expect(screen.getByText(/layout.welcome-to-eigent/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/layout.how-can-i-help-you/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render task splitting UI when task is in to_sub_tasks state', async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore, projectStore } = result.current
|
||||
const projectId = projectStore.activeProjectId as string
|
||||
const taskId = chatStore?.activeTaskId
|
||||
|
||||
if (!chatStore || !taskId) {
|
||||
throw new Error('ChatStore or taskId is null')
|
||||
}
|
||||
|
||||
// Simulate the state after receiving to_sub_tasks SSE event
|
||||
await act(async () => {
|
||||
chatStore.setHasMessages(taskId, true)
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'user-msg-1',
|
||||
role: 'user',
|
||||
content: 'Build a calculator app',
|
||||
attaches: []
|
||||
})
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'assistant-msg-1',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Calculator App|Build a simple calculator app',
|
||||
sub_tasks: [
|
||||
{ id: 'task-1', content: 'Create UI components', status: '' },
|
||||
{ id: 'task-2', content: 'Implement calculator logic', status: '' }
|
||||
]
|
||||
}
|
||||
})
|
||||
rerender()
|
||||
})
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
// Should show the task card/splitting interface
|
||||
await waitFor(() => {
|
||||
// Look for elements that indicate task splitting UI - there might be multiple instances
|
||||
const calculatorElements = screen.getAllByText('Build a calculator app')
|
||||
expect(calculatorElements.length).toBeGreaterThanOrEqual(1)
|
||||
// The component should show task breakdown
|
||||
expect(screen.queryByText(/layout.welcome-to-eigent/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render active conversation with messages', async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore?.activeTaskId
|
||||
|
||||
if (!chatStore || !taskId) {
|
||||
throw new Error('ChatStore or taskId is null')
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
chatStore.setHasMessages(taskId, true)
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
content: 'Hello, how are you?',
|
||||
attaches: []
|
||||
})
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'I am doing well, thank you! layout.how-can-i-help-you?',
|
||||
attaches: []
|
||||
})
|
||||
rerender()
|
||||
})
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Hello, how are you?')).toBeInTheDocument()
|
||||
expect(screen.getByText('I am doing well, thank you! layout.how-can-i-help-you?')).toBeInTheDocument()
|
||||
expect(screen.queryByText(/layout.welcome-to-eigent/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show loading state when task is pending', async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore?.activeTaskId
|
||||
|
||||
if (!chatStore || !taskId) {
|
||||
throw new Error('ChatStore or taskId is null')
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
chatStore.setHasMessages(taskId, true)
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
content: 'Calculate 2+2',
|
||||
attaches: []
|
||||
})
|
||||
chatStore.setIsPending(taskId, true)
|
||||
rerender()
|
||||
})
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Calculate 2+2')).toBeInTheDocument()
|
||||
// Should show some loading indicator - adjust this based on actual UI
|
||||
// For now, just check that we don't show the welcome screen
|
||||
expect(screen.queryByText(/layout.welcome-to-eigent/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should render file attachments when message has fileList', async () => {
|
||||
// This test requires understanding the exact structure of fileList handling
|
||||
// Skipping for now until we understand the component's file attachment UI
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore?.activeTaskId
|
||||
|
||||
if (!chatStore || !taskId) {
|
||||
throw new Error('ChatStore or taskId is null')
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
chatStore.setHasMessages(taskId, true)
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
content: 'Generate a report',
|
||||
attaches: []
|
||||
})
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'I have generated the report for you.',
|
||||
step: 'end',
|
||||
fileList: [
|
||||
{
|
||||
name: 'report.pdf',
|
||||
type: 'PDF',
|
||||
path: '/tmp/report.pdf'
|
||||
},
|
||||
{
|
||||
name: 'data.csv',
|
||||
type: 'CSV',
|
||||
path: '/tmp/data.csv'
|
||||
}
|
||||
]
|
||||
})
|
||||
rerender()
|
||||
})
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Generate a report')).toBeInTheDocument()
|
||||
expect(screen.getByText('I have generated the report for you.')).toBeInTheDocument()
|
||||
// These might be rendered differently in the actual component
|
||||
// expect(screen.getByText(/report\.pdf/i)).toBeInTheDocument()
|
||||
// expect(screen.getByText(/data\.csv/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should handle activeAsk state (waiting for human reply)', async () => {
|
||||
// This test requires understanding the exact input field structure
|
||||
// Skipping for now until we understand the component's input handling
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore?.activeTaskId
|
||||
|
||||
if (!chatStore || !taskId) {
|
||||
throw new Error('ChatStore or taskId is null')
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
chatStore.setHasMessages(taskId, true)
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
content: 'Help me decide between options',
|
||||
attaches: []
|
||||
})
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Which option would you prefer: A or B?',
|
||||
agent_name: 'decision-agent'
|
||||
})
|
||||
chatStore.setActiveAsk(taskId, 'decision-agent')
|
||||
rerender()
|
||||
})
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Help me decide between options')).toBeInTheDocument()
|
||||
expect(screen.getByText('Which option would you prefer: A or B?')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multi-ChatStore Project UI', () => {
|
||||
it.skip('should display multiple chat sessions in project view', async () => {
|
||||
// This test requires understanding the startTask method and multi-store handling
|
||||
// Skipping for now - needs investigation into the actual startTask implementation
|
||||
})
|
||||
|
||||
it.skip('should handle queued messages UI when task is busy', async () => {
|
||||
// This test requires understanding the queueing mechanism in the UI
|
||||
// Skipping for now - needs investigation into the actual queue handling
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error States and Edge Cases', () => {
|
||||
it('should handle corrupted chatStore state gracefully', async () => {
|
||||
// Test that the component doesn't crash when chatStore is in an invalid state
|
||||
// This is more of a safety test to ensure the component has proper error boundaries
|
||||
|
||||
// Should not crash when rendering with potentially corrupted state
|
||||
expect(() => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
}).not.toThrow()
|
||||
|
||||
// Should show some content (either welcome screen or handle the error gracefully)
|
||||
expect(screen.getByText(/layout.welcome-to-eigent/i) || screen.getByText(/error/i) || screen.getByRole('main')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should handle missing activeTaskId gracefully', async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore } = result.current
|
||||
|
||||
if (!chatStore) {
|
||||
// If chatStore is null, that's fine for this test
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
expect(screen.getByText(/layout.welcome-to-eigent/i)).toBeInTheDocument()
|
||||
return
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
// Try to set activeTaskId to null
|
||||
try {
|
||||
(chatStore as any).activeTaskId = null
|
||||
rerender()
|
||||
} catch (error) {
|
||||
// Expected - the store might prevent this
|
||||
}
|
||||
})
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interaction Flows', () => {
|
||||
it('should show textarea input field with correct placeholder', async () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
// Based on the error messages, the actual placeholder is "chat.ask-placeholder"
|
||||
// This appears to be a translation key rather than plain text
|
||||
const textarea = screen.getByPlaceholderText('chat.ask-placeholder')
|
||||
expect(textarea).toBeInTheDocument()
|
||||
expect(textarea.tagName).toBe('TEXTAREA')
|
||||
})
|
||||
|
||||
it('should have send button that is initially disabled', async () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
// Look for disabled send button (has arrow-right icon based on HTML structure)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const sendButton = buttons.find(btn =>
|
||||
btn.querySelector('svg.lucide-arrow-right') &&
|
||||
btn.hasAttribute('disabled')
|
||||
)
|
||||
|
||||
expect(sendButton).toBeInTheDocument()
|
||||
expect(sendButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should display layout.terms-of-use and layout.privacy-policy links', async () => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ChatBox />
|
||||
</TestWrapper>
|
||||
)
|
||||
|
||||
const termsLink = screen.getByRole('link', { name: /layout.terms-of-use/i })
|
||||
const privacyLink = screen.getByRole('link', { name: /layout.privacy-policy/i })
|
||||
|
||||
expect(termsLink).toBeInTheDocument()
|
||||
expect(termsLink).toHaveAttribute('href', 'https://www.eigent.ai/terms-of-use')
|
||||
expect(termsLink).toHaveAttribute('target', '_blank')
|
||||
|
||||
expect(privacyLink).toBeInTheDocument()
|
||||
expect(privacyLink).toHaveAttribute('href', 'https://www.eigent.ai/privacy-policy')
|
||||
expect(privacyLink).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with useChatStoreAdapter', () => {
|
||||
it('should properly integrate with useChatStoreAdapter hook', async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore, projectStore } = result.current
|
||||
|
||||
// Verify that the adapter returns the expected structure
|
||||
expect(projectStore).toBeDefined()
|
||||
expect(chatStore).toBeDefined()
|
||||
|
||||
// Verify that we can access basic properties
|
||||
if (chatStore) {
|
||||
expect(chatStore.activeTaskId).toBeDefined()
|
||||
expect(typeof chatStore.activeTaskId).toBe('string')
|
||||
}
|
||||
|
||||
if (projectStore) {
|
||||
expect(projectStore.activeProjectId).toBeDefined()
|
||||
expect(typeof projectStore.activeProjectId).toBe('string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle chatStore state changes through adapter', async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
const { chatStore } = result.current
|
||||
const taskId = chatStore?.activeTaskId
|
||||
|
||||
if (!chatStore || !taskId) {
|
||||
throw new Error('ChatStore or taskId is null')
|
||||
}
|
||||
|
||||
// Test adding a message through the adapter
|
||||
await act(async () => {
|
||||
chatStore.setHasMessages(taskId, true)
|
||||
chatStore.addMessages(taskId, {
|
||||
id: 'test-message-1',
|
||||
role: 'user',
|
||||
content: 'Test message from adapter',
|
||||
attaches: []
|
||||
})
|
||||
rerender()
|
||||
})
|
||||
|
||||
// Verify the message was added
|
||||
const updatedChatStore = result.current.chatStore
|
||||
if (updatedChatStore && updatedChatStore.tasks[taskId]) {
|
||||
expect(updatedChatStore.tasks[taskId].hasMessages).toBe(true)
|
||||
expect(updatedChatStore.tasks[taskId].messages).toHaveLength(1)
|
||||
expect(updatedChatStore.tasks[taskId].messages[0].content).toBe('Test message from adapter')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
33
test/mocks/authStore.mock.ts
Normal file
33
test/mocks/authStore.mock.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { vi } from "vitest";
|
||||
|
||||
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(() => [])
|
||||
}))
|
||||
64
test/mocks/proxy.mock.ts
Normal file
64
test/mocks/proxy.mock.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { vi } from "vitest"
|
||||
|
||||
// Mock API calls for both relative and alias paths
|
||||
const mockImplementation = {
|
||||
fetchPost: vi.fn((url, data) => {
|
||||
if (url.includes('/task/')) {
|
||||
return Promise.resolve({ success: true })
|
||||
}
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
fetchPut: vi.fn(() => Promise.resolve({ success: true })),
|
||||
getBaseURL: vi.fn(() => Promise.resolve('http://localhost:8000')),
|
||||
proxyFetchPost: vi.fn((url, data) => {
|
||||
// Mock history creation
|
||||
if (url.includes('/api/chat/history')) {
|
||||
return Promise.resolve({ id: 'history-' + Date.now() })
|
||||
}
|
||||
// Mock provider info
|
||||
if (url.includes('/api/providers')) {
|
||||
return Promise.resolve({ items: [] })
|
||||
}
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
proxyFetchPut: vi.fn(() => Promise.resolve({ success: true })),
|
||||
proxyFetchGet: vi.fn((url, params) => {
|
||||
// Mock user key
|
||||
if (url.includes('/api/user/key')) {
|
||||
return Promise.resolve({
|
||||
value: 'test-api-key',
|
||||
api_url: 'https://api.openai.com',
|
||||
})
|
||||
}
|
||||
// Mock providers
|
||||
if (url.includes('/api/providers')) {
|
||||
return Promise.resolve({ items: [] })
|
||||
}
|
||||
// Mock privacy settings
|
||||
if (url.includes('/api/user/privacy')) {
|
||||
return Promise.resolve({
|
||||
dataCollection: true,
|
||||
analytics: true,
|
||||
marketing: true
|
||||
})
|
||||
}
|
||||
// Mock configs
|
||||
if (url.includes('/api/configs')) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Mock snapshots - return empty array to prevent the error
|
||||
if (url.includes('/api/chat/snapshots')) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
uploadFile: vi.fn(),
|
||||
fetchDelete: vi.fn(),
|
||||
}
|
||||
|
||||
// Mock both relative and alias paths
|
||||
vi.mock('../../src/api/http', () => mockImplementation)
|
||||
vi.mock('@/api/http', () => mockImplementation)
|
||||
|
||||
// Export the mocked functions for use in tests
|
||||
export const { proxyFetchGet, proxyFetchPost, fetchPost } = mockImplementation
|
||||
8
test/mocks/sse.mock.ts
Normal file
8
test/mocks/sse.mock.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { vi } from "vitest"
|
||||
|
||||
// Mock fetchEventSource
|
||||
export const mockFetchEventSource = vi.fn()
|
||||
|
||||
vi.mock('@microsoft/fetch-event-source', () => ({
|
||||
fetchEventSource: (...args: any[]) => mockFetchEventSource(...args),
|
||||
}))
|
||||
|
|
@ -30,6 +30,10 @@ vi.mock('react-i18next', () => ({
|
|||
changeLanguage: vi.fn(),
|
||||
},
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock Electron APIs if needed
|
||||
|
|
|
|||
486
test/unit/store/chatStore.test.ts
Normal file
486
test/unit/store/chatStore.test.ts
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue