mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-14 16:42:47 +00:00
Merge branch 'main' into 621-bug-react-warning-confirmbuttondisabled-prop-passed-to-dom-element
This commit is contained in:
commit
a5e35d6f03
10 changed files with 716 additions and 145 deletions
|
|
@ -1180,57 +1180,57 @@ async function createWindow() {
|
|||
|
||||
// Handle localStorage based on installation state
|
||||
if (needsInstallation) {
|
||||
log.info('Installation needed - clearing auth storage to force carousel state');
|
||||
|
||||
// Clear the persisted auth storage file to force fresh initialization with carousel
|
||||
// Main window uses partition 'persist:main_window', so data is in Partitions/main_window
|
||||
const partitionPath = path.join(app.getPath('userData'), 'Partitions', 'main_window');
|
||||
const localStoragePath = path.join(partitionPath, 'Local Storage');
|
||||
const leveldbPath = path.join(localStoragePath, 'leveldb');
|
||||
|
||||
try {
|
||||
// Delete the localStorage database to force fresh init
|
||||
if (fs.existsSync(leveldbPath)) {
|
||||
log.info('Removing localStorage database to force fresh state...');
|
||||
fs.rmSync(leveldbPath, { recursive: true, force: true });
|
||||
log.info('Successfully cleared localStorage');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error clearing localStorage:', error);
|
||||
}
|
||||
log.info('Installation needed - resetting initState to carousel while preserving auth data');
|
||||
|
||||
// Instead of deleting the entire localStorage, we'll update only the initState
|
||||
// This preserves login information while resetting the initialization flow
|
||||
// Set up the injection for when page loads
|
||||
win.webContents.once('dom-ready', () => {
|
||||
if (!win || win.isDestroyed()) {
|
||||
log.warn('Window destroyed before DOM ready - skipping localStorage injection');
|
||||
return;
|
||||
}
|
||||
log.info('DOM ready - creating auth-storage with carousel state');
|
||||
log.info('DOM ready - updating initState to carousel while preserving auth data');
|
||||
win.webContents.executeJavaScript(`
|
||||
(function() {
|
||||
try {
|
||||
// Create fresh auth storage with carousel state
|
||||
const newAuthStorage = {
|
||||
state: {
|
||||
token: null,
|
||||
username: null,
|
||||
email: null,
|
||||
user_id: null,
|
||||
appearance: 'light',
|
||||
language: 'system',
|
||||
isFirstLaunch: true,
|
||||
modelType: 'cloud',
|
||||
cloud_model_type: 'gpt-4.1',
|
||||
initState: 'carousel',
|
||||
share_token: null,
|
||||
workerListData: {}
|
||||
},
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
|
||||
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
// Preserve existing auth data, only update initState
|
||||
const parsed = JSON.parse(authStorage);
|
||||
const updatedStorage = {
|
||||
...parsed,
|
||||
state: {
|
||||
...parsed.state,
|
||||
initState: 'carousel'
|
||||
}
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(updatedStorage));
|
||||
console.log('[ELECTRON PRE-INJECT] Updated initState to carousel, preserved auth data');
|
||||
} else {
|
||||
// No existing storage, create new one with carousel state
|
||||
const newAuthStorage = {
|
||||
state: {
|
||||
token: null,
|
||||
username: null,
|
||||
email: null,
|
||||
user_id: null,
|
||||
appearance: 'light',
|
||||
language: 'system',
|
||||
isFirstLaunch: true,
|
||||
modelType: 'cloud',
|
||||
cloud_model_type: 'gpt-4.1',
|
||||
initState: 'carousel',
|
||||
share_token: null,
|
||||
workerListData: {}
|
||||
},
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
|
||||
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
|
||||
console.error('[ELECTRON PRE-INJECT] Failed to update storage:', e);
|
||||
}
|
||||
})();
|
||||
`).catch(err => {
|
||||
|
|
@ -1282,9 +1282,9 @@ async function createWindow() {
|
|||
}
|
||||
})();
|
||||
`).then(needsReload => {
|
||||
if (needsReload) {
|
||||
if (needsReload && win && !win.isDestroyed()) {
|
||||
log.info('Reloading window after localStorage update');
|
||||
win!.reload();
|
||||
win.reload();
|
||||
}
|
||||
}).catch(err => {
|
||||
log.error('Failed to inject script:', err);
|
||||
|
|
|
|||
|
|
@ -102,7 +102,6 @@ export default function ChatBox(): JSX.Element {
|
|||
const task = chatStore.tasks[_taskId];
|
||||
const isTaskBusy = (
|
||||
// running or paused counts as busy
|
||||
// TODO: Bug where when replay end hasMessages = false & status = running
|
||||
(task.status === 'running' && !task.hasMessages) || task.status === 'pause' ||
|
||||
// splitting phase: has to_sub_tasks not confirmed OR skeleton computing
|
||||
task.messages.some(m => m.step === 'to_sub_tasks' && !m.isConfirm) ||
|
||||
|
|
@ -111,6 +110,10 @@ export default function ChatBox(): JSX.Element {
|
|||
(!!task.messages.find(m => m.step === 'to_sub_tasks' && !m.isConfirm) && task.status === 'pending')
|
||||
);
|
||||
|
||||
//Fixes bug where doesn't matter the SSE order or final state of the chatStore
|
||||
const isReplayChatStore = chatStore.tasks[_taskId as string]?.type === "replay";
|
||||
const queueTask = isTaskBusy && !isReplayChatStore;
|
||||
|
||||
console.log(`Current task is ${isTaskBusy} with ${task}`);
|
||||
|
||||
if (textareaRef.current) textareaRef.current.style.height = "60px";
|
||||
|
|
@ -152,7 +155,7 @@ export default function ChatBox(): JSX.Element {
|
|||
}
|
||||
} else {
|
||||
// If current task is busy (splitting/confirm/running), queue the new message instead of sending immediately
|
||||
if (isTaskBusy) {
|
||||
if (queueTask) {
|
||||
const project_id = projectStore.activeProjectId;
|
||||
// Queue the message locally; do not send to backend yet.
|
||||
const currentAttaches = JSON.parse(JSON.stringify(task.attaches)) || [];
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
? `${agentMap[data.type]?.borderColor} z-50`
|
||||
: "border-worker-border-default z-10"
|
||||
} transition-all duration-300 ease-in-out ${
|
||||
data.agent?.tasks.length === 0 && "opacity-30"
|
||||
(data.agent?.tasks?.length ?? 0) === 0 && "opacity-30"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
|
|
@ -451,13 +451,15 @@ export function Node({ id, data }: NodeProps) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{data.type === "document_agent" && (
|
||||
<div className="overflow-hidden w-full h-[180px] rounded-sm relative">
|
||||
<div className="absolute left-0 top-0 scale-[0.3] w-[500px] h-[500px] origin-top-left">
|
||||
<Folder data={data.agent as Agent} />
|
||||
{data.type === "document_agent" &&
|
||||
data?.agent?.tasks &&
|
||||
data.agent.tasks.length > 0 && (
|
||||
<div className="overflow-hidden w-full h-[180px] rounded-sm relative">
|
||||
<div className="absolute left-0 top-0 scale-[0.3] w-[500px] h-[500px] origin-top-left">
|
||||
<Folder data={data.agent as Agent} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{data.type === "developer_agent" &&
|
||||
data?.agent?.tasks &&
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { ChatStore } from "@/store/chatStore";
|
||||
import { ProjectStore } from "@/store/projectStore";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
|
||||
/**
|
||||
|
|
@ -11,7 +13,7 @@ import { NavigateFunction } from "react-router-dom";
|
|||
* @param historyId - The history ID for the replay
|
||||
*/
|
||||
export const replayProject = async (
|
||||
projectStore: any,
|
||||
projectStore: ProjectStore,
|
||||
navigate: NavigateFunction,
|
||||
projectId: string,
|
||||
question: string,
|
||||
|
|
@ -36,8 +38,8 @@ export const replayProject = async (
|
|||
* @param navigate - The navigate function from useNavigate hook
|
||||
*/
|
||||
export const replayActiveTask = async (
|
||||
chatStore: any,
|
||||
projectStore: any,
|
||||
chatStore: ChatStore,
|
||||
projectStore: ProjectStore,
|
||||
navigate: NavigateFunction
|
||||
) => {
|
||||
const taskId = chatStore.activeTaskId as string;
|
||||
|
|
@ -57,9 +59,10 @@ export const replayActiveTask = async (
|
|||
if (project && project.chatStores) {
|
||||
Object.entries(project.chatStores).forEach(([chatStoreId, chatStoreData]: [string, any]) => {
|
||||
const timestamp = project.chatStoreTimestamps[chatStoreId] || 0;
|
||||
|
||||
if (chatStoreData.tasks) {
|
||||
Object.values(chatStoreData.tasks).forEach((task: any) => {
|
||||
const chatState = chatStoreData.getState();
|
||||
|
||||
if (chatState.tasks) {
|
||||
Object.values(chatState.tasks).forEach((task: any) => {
|
||||
// Check messages for user content
|
||||
if (task.messages && task.messages.length > 0) {
|
||||
const userMessage = task.messages.find((msg: any) => msg.role === 'user');
|
||||
|
|
@ -68,33 +71,6 @@ export const replayActiveTask = async (
|
|||
earliestTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// Check task info for original questions
|
||||
if (task.taskInfo && task.taskInfo.length > 0) {
|
||||
task.taskInfo.forEach((info: any) => {
|
||||
if (info.content && timestamp < earliestTimestamp) {
|
||||
question = info.content;
|
||||
earliestTimestamp = timestamp;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check agent logs for original task content
|
||||
if (task.taskAssigning) {
|
||||
task.taskAssigning.forEach((agent: any) => {
|
||||
if (agent.log) {
|
||||
agent.log.forEach((logEntry: any) => {
|
||||
if (logEntry.data && logEntry.data.message) {
|
||||
const match = logEntry.data.message.match(/==============================\n(.*?)\n==============================/s);
|
||||
if (match && match[1] && timestamp < earliestTimestamp) {
|
||||
question = match[1].trim();
|
||||
earliestTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -103,6 +79,7 @@ export const replayActiveTask = async (
|
|||
// Fallback to current task's first message if no question found
|
||||
if (!question && chatStore.tasks[taskId] && chatStore.tasks[taskId].messages[0]) {
|
||||
question = chatStore.tasks[taskId].messages[0].content;
|
||||
console.log("[REPLAY] question fall back to ", question);
|
||||
}
|
||||
|
||||
const historyId = projectStore.getHistoryId(projectId);
|
||||
|
|
|
|||
|
|
@ -196,7 +196,8 @@ const projectStore = create<ProjectStore>()((set, get) => ({
|
|||
queuedMessages: [], // Initialize empty queued messages array
|
||||
metadata: {
|
||||
status: 'active',
|
||||
historyId: historyId
|
||||
historyId: historyId,
|
||||
tags: type === ProjectType.REPLAY ? ["replay"] : []
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -472,27 +473,34 @@ const projectStore = create<ProjectStore>()((set, get) => ({
|
|||
}
|
||||
|
||||
console.log(`[ProjectStore] Created replay project ${replayProjectId} for ${taskIds.length} tasks`);
|
||||
|
||||
|
||||
// For each taskId, create a chat store within the project and call replay
|
||||
taskIds.forEach(async (taskId, index) => {
|
||||
console.log(`[ProjectStore] Creating replay for task ${index + 1}/${taskIds.length}: ${taskId}`);
|
||||
|
||||
// Create a new chat store for this task
|
||||
const chatId = createChatStore(replayProjectId, `Task ${taskId}`);
|
||||
|
||||
if (chatId) {
|
||||
const project = get().projects[replayProjectId];
|
||||
const chatStore = project.chatStores[chatId];
|
||||
|
||||
if (chatStore) {
|
||||
// Call replay on the chat store with the taskId, question, and 0 delay
|
||||
await chatStore.getState().replay(taskId, question, 0.2);
|
||||
console.log(`[ProjectStore] Started replay for task ${taskId}`);
|
||||
(async () => {
|
||||
for (let index = 0; index < taskIds.length; index++) {
|
||||
const taskId = taskIds[index];
|
||||
console.log(`[ProjectStore] Creating replay for task ${index + 1}/${taskIds.length}: ${taskId}`);
|
||||
|
||||
// Create a new chat store for this task
|
||||
const chatId = createChatStore(replayProjectId, `Task ${taskId}`);
|
||||
|
||||
if (chatId) {
|
||||
const project = get().projects[replayProjectId];
|
||||
const chatStore = project.chatStores[chatId];
|
||||
|
||||
if (chatStore) {
|
||||
// Call replay on the chat store with the taskId, question, and 0 delay
|
||||
try {
|
||||
await chatStore.getState().replay(taskId, question, 0.2);
|
||||
console.log(`[ProjectStore] Started replay for task ${taskId}`);
|
||||
} catch (error) {
|
||||
console.error(`[ProjectStore] Failed to replay task ${taskId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[ProjectStore] Completed replay setup for ${taskIds.length} tasks`);
|
||||
console.log(`[ProjectStore] Completed replay setup for ${taskIds.length} tasks`);
|
||||
})();
|
||||
|
||||
return replayProjectId;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -13,26 +13,7 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
import { mockFetchEventSource, createSSESequence } from '../../mocks/sse.mock'
|
||||
|
||||
// Mock electron IPC
|
||||
(global as any).ipcRenderer = {
|
||||
|
|
|
|||
|
|
@ -13,27 +13,8 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
import { createSSESequence, issue619SseSequence, mockFetchEventSource } from '../../mocks/sse.mock'
|
||||
import { replayProject, replayActiveTask } from '../../../src/lib'
|
||||
|
||||
// Mock navigate function
|
||||
const mockNavigate = vi.fn() as any
|
||||
|
|
@ -523,4 +504,304 @@ describe('Integration Test: Replay Functionality', () => {
|
|||
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 } = 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("/")
|
||||
})
|
||||
})
|
||||
|
|
@ -5,4 +5,323 @@ export const mockFetchEventSource = vi.fn()
|
|||
|
||||
vi.mock('@microsoft/fetch-event-source', () => ({
|
||||
fetchEventSource: (...args: any[]) => mockFetchEventSource(...args),
|
||||
}))
|
||||
}))
|
||||
|
||||
// Helper function for sequential SSE events
|
||||
export 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// https://github.com/eigent-ai/eigent/issues/619 - Two task boxes
|
||||
export const issue619SseSequence = [
|
||||
{
|
||||
event: {
|
||||
step: 'confirmed',
|
||||
data: { question: 'Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.' }
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Task|Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.',
|
||||
sub_tasks: [
|
||||
{
|
||||
id: '1762503273000-2985.1',
|
||||
content: 'Retrieve the details of the next scheduled event on the user\'s Google Calendar, including the start time, title of the meeting, and the list of all participants\' names and email addresses. Format the result as a JSON object with the keys: \'start_time\', \'meeting_title\', and \'participants\', where \'participants\' is an array of objects each with \'name\' and \'email\'.',
|
||||
state: 'FAILED',
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'deactivate_agent',
|
||||
data: {
|
||||
agent_name: 'question_confirm_agent',
|
||||
agent_id: '3f9fff03-e95a-4ad6-a3d4-6bdd73b639c4',
|
||||
process_task_id: '',
|
||||
message: 'yes',
|
||||
tokens: 357
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'task_agent',
|
||||
agent_id: '17cef41b-5158-40e8-b1e2-0a88777dddb4',
|
||||
tools: []
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
agent_id: 'c249ac7b-6745-4832-a5c1-3238f9176434',
|
||||
tools: ['Search Toolkit', 'Browser Toolkit', 'Human Toolkit', 'Note Taking Toolkit', 'Terminal Toolkit']
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'deactivate_toolkit',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Browser Toolkit',
|
||||
method_name: 'register agent',
|
||||
message: 'null'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'document_agent',
|
||||
agent_id: 'e861cc5b-976b-41f0-a298-9c3e921ffaf5',
|
||||
tools: ['File Toolkit', 'Pptx Toolkit', 'Human Toolkit', 'Mark It Down Toolkit', 'Excel Toolkit', 'Note Taking Toolkit', 'Terminal Toolkit', 'Google Drive Mcp Toolkit']
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'deactivate_toolkit',
|
||||
data: {
|
||||
agent_name: 'multi_modal_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Open Ai Image Toolkit',
|
||||
method_name: 'get openai credentials',
|
||||
message: '[object Object]'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'activate_toolkit',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Browser Toolkit',
|
||||
method_name: 'register agent',
|
||||
message: 'ChatAgent(Search Agent, RoleType.ASSISTANT, gpt-5)'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'mcp_agent',
|
||||
agent_id: 'd23a70fe-5c90-47bd-9170-f50defcd352a',
|
||||
tools: []
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'activate_toolkit',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Browser Toolkit',
|
||||
method_name: 'register agent',
|
||||
message: 'ChatAgent(Search Agent, RoleType.ASSISTANT, gpt-5)'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'activate_agent',
|
||||
data: {
|
||||
agent_name: 'question_confirm_agent',
|
||||
process_task_id: '',
|
||||
agent_id: '3f9fff03-e95a-4ad6-a3d4-6bdd73b639c4',
|
||||
message: '=== Previous Conversation ===\nPrevious Task: Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.\n\n\nUser Query: Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.\n\nDetermine if this user query is a complex task or a simple question.\n\nComplex task (answer "yes"): Requires tools, code execution, file operations, multi-step planning, or creating/modifying content\n- Examples: "create a file", "search for X", "implement feature Y", "write code", "analyze data", "build something"\n\nSimple question (answer "no"): Can be answered directly with knowledge or conversation history, no action needed\n- Examples: greetings ("hello", "hi"), fact queries ("what is X?"), clarifications ("what did you mean?"), status checks ("how are you?")\n\nAnswer only "yes" or "no". Do not provide any explanation.\n\nIs this a complex task? (yes/no):'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'coordinator_agent',
|
||||
agent_id: '97326e19-91aa-4f9e-95ac-dbc1d92d3756',
|
||||
tools: []
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'new_worker_agent',
|
||||
agent_id: '62eca7d0-ed56-4101-bd20-9c929d31590e',
|
||||
tools: []
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'activate_toolkit',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Browser Toolkit',
|
||||
method_name: 'register agent',
|
||||
message: 'ChatAgent(Search Agent, RoleType.ASSISTANT, gpt-4.1)'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'developer_agent',
|
||||
agent_id: 'd7514c96-3414-4220-9a99-c08cbabc86b5',
|
||||
tools: ['Human Toolkit', 'Terminal Toolkit', 'Note Taking Toolkit', 'Web Deploy Toolkit']
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'activate_toolkit',
|
||||
data: {
|
||||
agent_name: 'multi_modal_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Open Ai Image Toolkit',
|
||||
method_name: 'get openai credentials',
|
||||
message: '[object Object]'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'multi_modal_agent',
|
||||
agent_id: '2682c381-3560-4584-96c6-985a5395c85d',
|
||||
tools: ['Video Downloader Toolkit', 'Audio Analysis Toolkit', 'Image Analysis Toolkit', 'Open Ai Image Toolkit', 'Human Toolkit', 'Terminal Toolkit', 'Note Taking Toolkit', 'Search Toolkit']
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'deactivate_toolkit',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Browser Toolkit',
|
||||
method_name: 'register agent',
|
||||
message: 'null'
|
||||
}
|
||||
},
|
||||
delay: 100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'create_agent',
|
||||
data: {
|
||||
agent_name: 'Google calendar agent ',
|
||||
agent_id: '729ebd9b-4eef-4f4a-83b8-0289bef302c5',
|
||||
tools: ['Google Calendar Toolkit']
|
||||
}
|
||||
},
|
||||
delay: 1100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'activate_toolkit',
|
||||
data: {
|
||||
agent_name: 'search_agent',
|
||||
process_task_id: '',
|
||||
toolkit_name: 'Browser Toolkit',
|
||||
method_name: 'register agent',
|
||||
message: 'ChatAgent(Search Agent, RoleType.ASSISTANT, gpt-4.1)'
|
||||
}
|
||||
},
|
||||
delay: 1100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'deactivate_toolkit',
|
||||
data: {
|
||||
agent_name: 'Google calendar agent ',
|
||||
process_task_id: '1762502461718-31.1',
|
||||
toolkit_name: 'Google Calendar Toolkit',
|
||||
method_name: 'get calendar details',
|
||||
message: 'Failed to retrieve calendar details'
|
||||
}
|
||||
},
|
||||
delay: 1100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'deactivate_toolkit',
|
||||
data: {
|
||||
agent_name: 'Google calendar agent ',
|
||||
process_task_id: '1762502461718-31.1',
|
||||
toolkit_name: 'Google Calendar Toolkit',
|
||||
method_name: 'get events',
|
||||
message: '{"error": "Failed to retrieve events: timed out"}'
|
||||
}
|
||||
},
|
||||
delay: 1100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'to_sub_tasks',
|
||||
data: {
|
||||
summary_task: 'Task|Please help me check Google Calendar when is the next meeting, what kind of meeting it is, and who is attending the meeting.',
|
||||
sub_tasks: []
|
||||
}
|
||||
},
|
||||
delay: 1100
|
||||
},
|
||||
{
|
||||
event: {
|
||||
step: 'end',
|
||||
data: ''
|
||||
},
|
||||
delay: 1100
|
||||
}
|
||||
];
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue