eigent/test/integration/chatStore/newProject.test.tsx
Tong Chen af93bb3065
feat: Add Lint & Format (#878)
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>
2026-02-01 23:16:18 +08:00

409 lines
14 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. =========
/**
* 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);
});
});
});
});