mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-19 07:59:39 +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>
976 lines
30 KiB
TypeScript
976 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 { 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 useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter';
|
|
import { replayActiveTask, replayProject } from '../../../src/lib';
|
|
import { useProjectStore } from '../../../src/store/projectStore';
|
|
import {
|
|
createSSESequence,
|
|
issue619SseSequence,
|
|
mockFetchEventSource,
|
|
} from '../../mocks/sse.mock';
|
|
|
|
// 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 }
|
|
);
|
|
});
|
|
});
|
|
|
|
//https://github.com/eigent-ai/eigent/issues/619 - Two task boxes
|
|
describe('Issue #619 - Duplicate Task Boxes after replay', () => {
|
|
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 a separate chatStore for each replay', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter());
|
|
|
|
let sseCallCount = 0;
|
|
|
|
// Step 0: First simulate a replay mechanism to set up the scenario
|
|
const replaySequence = createSSESequence([
|
|
{
|
|
event: {
|
|
step: 'confirmed',
|
|
data: { question: 'Previous calendar task replay' },
|
|
},
|
|
delay: 50,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'to_sub_tasks',
|
|
data: {
|
|
summary_task: 'Calendar Replay|Previous calendar interaction',
|
|
sub_tasks: [
|
|
{
|
|
id: 'replay-cal-1',
|
|
content: 'Check calendar access',
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'replay-cal-2',
|
|
content: 'Fetch meeting data',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
delay: 100,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'end',
|
|
data: '--- Previous Calendar Task Replay Complete ---\nFound 3 upcoming meetings',
|
|
},
|
|
delay: 150,
|
|
},
|
|
]);
|
|
|
|
// 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 simulate replay of previous event to establish context
|
|
if (sseCallCount === 1 && url.includes('/api/chat/steps/playback/')) {
|
|
console.log(
|
|
'Simulating replay mechanism for previous calendar task'
|
|
);
|
|
await replaySequence(options.onmessage);
|
|
return;
|
|
}
|
|
|
|
// Now send the new task events with actual task IDs
|
|
const immediateSequence = createSSESequence(issue619SseSequence);
|
|
await immediateSequence(options.onmessage);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Simulate the replay mechanism first
|
|
console.log('Starting replay simulation to establish context...');
|
|
await act(async () => {
|
|
await replayProject(
|
|
result.current.projectStore,
|
|
mockNavigate,
|
|
generateUniqueId(),
|
|
'Previous calendar task replay',
|
|
'calendar-replay-history'
|
|
);
|
|
rerender();
|
|
});
|
|
|
|
// Wait for replay to complete and verify it's established
|
|
await waitFor(
|
|
() => {
|
|
rerender();
|
|
const { projectStore } = result.current;
|
|
const projects = projectStore.getAllProjects();
|
|
const replayProject = projects.find((p: any) =>
|
|
p.name.includes('Replay Project')
|
|
);
|
|
expect(replayProject).toBeDefined();
|
|
console.log('Replay mechanism completed - context established');
|
|
},
|
|
{ timeout: 1000 }
|
|
);
|
|
|
|
// 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 =
|
|
'Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.';
|
|
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(
|
|
'Task|Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.'
|
|
);
|
|
console.log('Task info is ', task.taskInfo);
|
|
//Bcz of newTaskInfo { id: '', content: '', status: '' } we have 2 items
|
|
expect(task.taskInfo).toHaveLength(2);
|
|
|
|
console.log('Task reached to_sub_tasks phase - now busy');
|
|
},
|
|
{ timeout: 1500 }
|
|
);
|
|
|
|
//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: 10000 }
|
|
);
|
|
|
|
// Step 7: Verify final state
|
|
const { chatStore: finalChatStore, projectStore: finalProjectStore } =
|
|
result.current;
|
|
const _finalProject = finalProjectStore.getProjectById(projectId);
|
|
|
|
// Verify task completed successfully
|
|
const finalTaskId = finalChatStore.activeTaskId;
|
|
const finalTask = finalChatStore.tasks[finalTaskId];
|
|
expect(finalTask.status).toBe('finished');
|
|
expect(finalTask.summaryTask).toBe(
|
|
'Task|Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.'
|
|
);
|
|
|
|
console.log(
|
|
'Test completed - queue management verified: one task processed, one remains'
|
|
);
|
|
});
|
|
|
|
it('should have correct first question on replayActiveTask', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter());
|
|
const _projectStoreResult = renderHook(() => useProjectStore());
|
|
|
|
let sseCallCount = 0;
|
|
const originalUserMessage =
|
|
'Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.';
|
|
|
|
// Step 1: Create initial task with specific user message
|
|
const initialSequence = createSSESequence([
|
|
{
|
|
event: {
|
|
step: 'confirmed',
|
|
data: { question: originalUserMessage },
|
|
},
|
|
delay: 100,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'to_sub_tasks',
|
|
data: {
|
|
summary_task:
|
|
'Calendar Task|Check upcoming Google Calendar meetings',
|
|
sub_tasks: [
|
|
{
|
|
id: 'cal-1',
|
|
content: 'Access Google Calendar',
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'cal-2',
|
|
content: 'Fetch meeting details',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
delay: 200,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'end',
|
|
data: '--- Calendar Task Complete ---\nFound your next meeting: Team Standup at 2:00 PM',
|
|
},
|
|
delay: 300,
|
|
},
|
|
]);
|
|
|
|
// Step 2: Setup replay sequence that should have same first question
|
|
const replaySequence = createSSESequence([
|
|
{
|
|
event: {
|
|
step: 'confirmed',
|
|
data: { question: 'Fall Back question' },
|
|
},
|
|
delay: 100,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'to_sub_tasks',
|
|
data: {
|
|
summary_task:
|
|
'Calendar Task Replay|Replaying calendar meeting check',
|
|
sub_tasks: [
|
|
{
|
|
id: 'replay-cal-1',
|
|
content: 'Access Google Calendar (replay)',
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'replay-cal-2',
|
|
content: 'Fetch meeting details (replay)',
|
|
status: 'completed',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
delay: 200,
|
|
},
|
|
{
|
|
event: {
|
|
step: 'end',
|
|
data: '--- Calendar Task Replay Complete ---\nReplayed: Found your next meeting',
|
|
},
|
|
delay: 300,
|
|
},
|
|
]);
|
|
|
|
// Mock SSE to handle both initial task and replay
|
|
mockFetchEventSource.mockImplementation(
|
|
async (url: string, options: any) => {
|
|
sseCallCount++;
|
|
console.log(`SSE Call #${sseCallCount}: ${url}`);
|
|
|
|
if (options.onmessage) {
|
|
if (sseCallCount === 1) {
|
|
// First call: initial task
|
|
console.log('Processing initial task events');
|
|
await initialSequence(options.onmessage);
|
|
} else if (url.includes('/api/chat/steps/playback/')) {
|
|
// Subsequent calls: replay
|
|
console.log('Processing replay events');
|
|
await replaySequence(options.onmessage);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Step 3: Start initial task
|
|
await act(async () => {
|
|
const { chatStore } = result.current;
|
|
const taskId = chatStore.activeTaskId;
|
|
await chatStore.startTask(
|
|
taskId,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
originalUserMessage
|
|
);
|
|
rerender();
|
|
});
|
|
|
|
// Wait for initial task to complete
|
|
await waitFor(
|
|
() => {
|
|
rerender();
|
|
const { chatStore } = result.current;
|
|
const taskId = chatStore.activeTaskId;
|
|
const task = chatStore.tasks[taskId];
|
|
expect(task.status).toBe('finished');
|
|
expect(task.messages[0].content).toBe(originalUserMessage);
|
|
console.log(
|
|
'Initial task completed with user message:',
|
|
task.messages[0].content
|
|
);
|
|
},
|
|
{ timeout: 2000 }
|
|
);
|
|
|
|
// Step 4: Get the completed chatStore for replay
|
|
const { chatStore: completedChatStore, projectStore } = result.current;
|
|
const completedTaskId = completedChatStore.activeTaskId;
|
|
const completedTask = completedChatStore.tasks[completedTaskId];
|
|
|
|
// Verify we have the correct initial state
|
|
expect(completedTask.messages[0].content).toBe(originalUserMessage);
|
|
expect(completedTask.status).toBe('finished');
|
|
|
|
// Step 5: Call replayActiveTask using the completed chatStore
|
|
await act(async () => {
|
|
await replayActiveTask(completedChatStore, projectStore, mockNavigate);
|
|
rerender();
|
|
});
|
|
|
|
// Step 6: Wait for replay to complete and verify first question matches
|
|
await waitFor(
|
|
() => {
|
|
rerender();
|
|
const { projectStore: updatedProjectStore } = result.current;
|
|
const projects = updatedProjectStore.getAllProjects();
|
|
|
|
// Find the replay project
|
|
const replayProject = projects.find((p: any) =>
|
|
p.name.includes('Replay Project')
|
|
);
|
|
expect(replayProject).toBeDefined();
|
|
|
|
// Get the replay chatStore
|
|
const replayChatStores = updatedProjectStore.getAllChatStores(
|
|
replayProject!.id
|
|
);
|
|
expect(replayChatStores.length).toBeGreaterThan(1);
|
|
|
|
const replayChatStore = replayChatStores[1].chatStore; // Skip the empty initial one
|
|
const replayTaskId = replayChatStore.getState().activeTaskId;
|
|
const replayTask = replayChatStore.getState().tasks[replayTaskId];
|
|
|
|
// THE MAIN TEST: First question in replay should match original user message
|
|
expect(replayTask).toBeDefined();
|
|
expect(replayTask.messages[0].content).toBe(originalUserMessage);
|
|
expect(replayTask.type).toBe('replay');
|
|
|
|
console.log(
|
|
'✅ Replay first question matches original:',
|
|
replayTask.messages[0].content
|
|
);
|
|
console.log('✅ Original user message:', originalUserMessage);
|
|
console.log(
|
|
'✅ Test passed: replayActiveTask preserves correct first question'
|
|
);
|
|
},
|
|
{ timeout: 3000 }
|
|
);
|
|
|
|
// Verify navigation was called for replay
|
|
expect(mockNavigate).toHaveBeenCalledWith('/');
|
|
});
|
|
});
|