// ========= 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. ========= /** * 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 { act, renderHook, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 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); }); }); }); });