mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-30 04:30:13 +00:00
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com> Co-authored-by: eigent-ai <camel@eigent.ai> Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
913 lines
30 KiB
TypeScript
913 lines
30 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
// 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 useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter';
|
|
import { useProjectStore } from '../../../src/store/projectStore';
|
|
import { createSSESequence, 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,
|
|
});
|
|
|
|
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, reject) => {
|
|
const startTime = Date.now();
|
|
const timeout = 5000; // 5 second timeout
|
|
const pollForIds = () => {
|
|
if (queuedTaskIds.length > 0) {
|
|
console.log(`Found queued task IDs: ${queuedTaskIds}`);
|
|
resolve();
|
|
} else if (Date.now() - startTime > timeout) {
|
|
reject(new Error('Timeout waiting for queued task IDs'));
|
|
} 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: currentProjectId,
|
|
},
|
|
},
|
|
delay: 100,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'remove_task',
|
|
data: {
|
|
task_id: queuedTaskIds[0], // Remove first task from queue
|
|
project_id: currentProjectId,
|
|
},
|
|
},
|
|
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([]);
|
|
|
|
// Store projectId in outer scope for use in SSE mock
|
|
let currentProjectId = projectId;
|
|
|
|
// 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: _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');
|
|
},
|
|
{ timeout: 2000 }
|
|
);
|
|
|
|
// 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: _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: _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: _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, reject) => {
|
|
const startTime = Date.now();
|
|
const timeout = 5000; // 5 second timeout
|
|
const pollForIds = () => {
|
|
if (comprehensiveQueuedTaskIds.length > 0) {
|
|
console.log(
|
|
`Found comprehensive queued task IDs: ${comprehensiveQueuedTaskIds}`
|
|
);
|
|
resolve();
|
|
} else if (Date.now() - startTime > timeout) {
|
|
reject(
|
|
new Error(
|
|
'Timeout waiting for comprehensive queued task IDs'
|
|
)
|
|
);
|
|
} 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');
|
|
},
|
|
{ timeout: 2000 }
|
|
);
|
|
|
|
// 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 allChatStores = finalProjectStore.getAllChatStores(
|
|
finalProjectStore.activeProjectId
|
|
);
|
|
// Should have initial + first task + second task = 3 chat stores
|
|
expect(allChatStores).toHaveLength(3);
|
|
const [_initial, first, _second] = allChatStores;
|
|
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'
|
|
);
|
|
});
|
|
});
|