/** * ZCLAW Memory System E2E Tests * * Tests for memory persistence, cross-session memory, and context compression. * Covers the agent memory management system integrated with chat. * * Test Categories: * - Conversation Persistence: Save, restore, navigate conversations * - Cross-Session Memory: Long-term memory, memory search * - Context Compression: Automatic context reduction, token management * - Memory Extraction: Extracting insights from conversations */ import { test, expect, Page } from '@playwright/test'; import { setupMockGateway, mockResponses, mockAgentMessageResponse } from '../fixtures/mock-gateway'; import { storeInspectors, STORAGE_KEYS } from '../fixtures/store-inspectors'; import { userActions, waitForAppReady, skipOnboarding, navigateToTab } from '../utils/user-actions'; // Test configuration test.setTimeout(120000); const BASE_URL = 'http://localhost:1420'; // Helper to generate unique IDs const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // ============================================ // Test Suite 1: Conversation Persistence Tests // ============================================ test.describe('Memory System - Conversation Persistence Tests', () => { test.describe.configure({ mode: 'parallel' }); test.beforeEach(async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); }); test('MEM-PERSIST-01: Conversation saves to localStorage', async ({ page }) => { // Clear existing conversations await storeInspectors.clearStore(page, 'CHAT'); await page.waitForTimeout(300); // Create a conversation with messages const conversationData = { messages: [ { id: `user_${Date.now()}`, role: 'user', content: 'Test message for persistence', timestamp: new Date().toISOString(), }, { id: `assistant_${Date.now()}`, role: 'assistant', content: 'Test response for persistence', timestamp: new Date().toISOString(), }, ], conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }; await storeInspectors.setChatState(page, conversationData); await page.waitForTimeout(500); // Verify data persisted const storedState = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; }>(page); expect(storedState).not.toBeNull(); expect(storedState?.messages?.length).toBe(2); expect(storedState?.messages[0]?.content).toBe('Test message for persistence'); }); test('MEM-PERSIST-02: Conversation persists across page reload', async ({ page }) => { // Set up initial conversation const testMessage = `Persistence test ${generateId()}`; await storeInspectors.setChatState(page, { messages: [ { id: 'msg-1', role: 'user', content: testMessage, timestamp: new Date().toISOString() }, ], conversations: [{ id: 'conv-1', title: 'Test Conversation', messages: [{ id: 'msg-1', role: 'user', content: testMessage, timestamp: new Date() } as any], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'conv-1', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Reload page await page.reload(); await waitForAppReady(page); // Verify message persisted const state = await storeInspectors.getChatState<{ conversations: Array<{ title: string }>; }>(page); expect(state?.conversations).toBeDefined(); expect(state?.conversations.length).toBeGreaterThan(0); }); test('MEM-PERSIST-03: Multiple conversations maintained', async ({ page }) => { // Create multiple conversations const conversations = [ { id: 'conv-1', title: 'First Conversation', messages: [ { id: 'msg-1', role: 'user', content: 'First conversation message', timestamp: new Date() } as any, ], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }, { id: 'conv-2', title: 'Second Conversation', messages: [ { id: 'msg-2', role: 'user', content: 'Second conversation message', timestamp: new Date() } as any, ], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }, { id: 'conv-3', title: 'Third Conversation', messages: [ { id: 'msg-3', role: 'user', content: 'Third conversation message', timestamp: new Date() } as any, ], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }, ]; await storeInspectors.setChatState(page, { messages: [], conversations, currentConversationId: 'conv-1', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); await page.waitForTimeout(500); // Verify all conversations stored const state = await storeInspectors.getChatState<{ conversations: Array<{ id: string; title: string }>; }>(page); expect(state?.conversations?.length).toBe(3); expect(state?.conversations?.map(c => c.title)).toContain('First Conversation'); expect(state?.conversations?.map(c => c.title)).toContain('Second Conversation'); expect(state?.conversations?.map(c => c.title)).toContain('Third Conversation'); }); test('MEM-PERSIST-04: Switch between conversations', async ({ page }) => { // Set up multiple conversations const conversations = [ { id: 'conv-a', title: 'Conversation A', messages: [ { id: 'msg-a', role: 'user', content: 'Message in A', timestamp: new Date() } as any, ], sessionKey: 'session-a', agentId: null, createdAt: new Date(), updatedAt: new Date(), }, { id: 'conv-b', title: 'Conversation B', messages: [ { id: 'msg-b', role: 'user', content: 'Message in B', timestamp: new Date() } as any, ], sessionKey: 'session-b', agentId: null, createdAt: new Date(), updatedAt: new Date(), }, ]; await storeInspectors.setChatState(page, { messages: [], conversations, currentConversationId: 'conv-a', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Switch conversation via store const switchResult = await page.evaluate(async () => { try { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.getState().switchConversation('conv-b'); return { success: true, currentId: stores.chat.getState().currentConversationId, }; } return { success: false }; } catch (e) { return { success: false, error: String(e) }; } }); expect(switchResult.success).toBe(true); expect(switchResult.currentId).toBe('conv-b'); }); test('MEM-PERSIST-05: Delete conversation removes from list', async ({ page }) => { // Set up conversations const conversations = [ { id: 'conv-delete', title: 'To Delete', messages: [], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }, { id: 'conv-keep', title: 'To Keep', messages: [], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }, ]; await storeInspectors.setChatState(page, { messages: [], conversations, currentConversationId: 'conv-delete', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Delete conversation const deleteResult = await page.evaluate(async () => { try { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.getState().deleteConversation('conv-delete'); return { success: true, remaining: stores.chat.getState().conversations.length, currentId: stores.chat.getState().currentConversationId, }; } return { success: false }; } catch (e) { return { success: false, error: String(e) }; } }); expect(deleteResult.success).toBe(true); expect(deleteResult.remaining).toBe(1); expect(deleteResult.currentId).toBeNull(); }); test('MEM-PERSIST-06: New conversation starts fresh', async ({ page }) => { // Set up existing conversation with messages await storeInspectors.setChatState(page, { messages: [ { id: 'old-msg', role: 'user', content: 'Old message', timestamp: new Date() } as any, ], conversations: [{ id: 'old-conv', title: 'Old Conversation', messages: [{ id: 'old-msg', role: 'user', content: 'Old message', timestamp: new Date() } as any], sessionKey: 'old-session', agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'old-conv', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: 'old-session', agents: [], }); // Create new conversation const newResult = await page.evaluate(async () => { try { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.getState().newConversation(); return { success: true, messagesCount: stores.chat.getState().messages.length, sessionKey: stores.chat.getState().sessionKey, isStreaming: stores.chat.getState().isStreaming, }; } return { success: false }; } catch (e) { return { success: false, error: String(e) }; } }); expect(newResult.success).toBe(true); expect(newResult.messagesCount).toBe(0); expect(newResult.sessionKey).toBeNull(); expect(newResult.isStreaming).toBe(false); }); }); // ============================================ // Test Suite 2: Cross-Session Memory Tests // ============================================ test.describe('Memory System - Cross-Session Memory Tests', () => { test.describe.configure({ mode: 'parallel' }); test.beforeEach(async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); }); test('MEM-CROSS-01: Session key maintains context', async ({ page }) => { const sessionKey = `session_${generateId()}`; // Set up conversation with session key await storeInspectors.setChatState(page, { messages: [ { id: 'msg-1', role: 'user', content: 'User preference: I prefer TypeScript', timestamp: new Date() } as any, { id: 'msg-2', role: 'assistant', content: 'Noted your preference for TypeScript', timestamp: new Date() } as any, ], conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey, agents: [], }); await page.waitForTimeout(500); // Verify session key persisted const state = await storeInspectors.getChatState<{ sessionKey: string; }>(page); expect(state?.sessionKey).toBe(sessionKey); }); test('MEM-CROSS-02: Agent identity persists across sessions', async ({ page }) => { // Set up agent await storeInspectors.setChatState(page, { messages: [], conversations: [], currentConversationId: null, currentAgent: { id: 'agent-custom', name: 'Custom Agent', icon: 'C', color: 'bg-blue-500', lastMessage: 'Hello', time: new Date().toISOString(), }, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Reload page await page.reload(); await waitForAppReady(page); // Verify agent persisted const state = await storeInspectors.getChatState<{ currentAgent: { id: string; name: string } | null; }>(page); // Agent ID should be persisted expect(state?.currentAgent).toBeDefined(); }); test('MEM-CROSS-03: Model selection persists', async ({ page }) => { // Set specific model const testModel = 'claude-3-haiku-20240307'; await storeInspectors.setChatState(page, { messages: [], conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: testModel, sessionKey: null, agents: [], }); // Reload page await page.reload(); await waitForAppReady(page); // Verify model persisted const state = await storeInspectors.getChatState<{ currentModel: string; }>(page); expect(state?.currentModel).toBe(testModel); }); test('MEM-CROSS-04: Long conversation history maintained', async ({ page }) => { // Create conversation with many messages const messages = []; for (let i = 0; i < 50; i++) { messages.push({ id: `msg-${i}`, role: i % 2 === 0 ? 'user' : 'assistant', content: `Message ${i} with some content to test persistence of long conversations`, timestamp: new Date(Date.now() + i * 1000), }); } const conversation = { id: 'conv-long', title: 'Long Conversation', messages: messages as any[], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }; await storeInspectors.setChatState(page, { messages: messages as any[], conversations: [conversation], currentConversationId: 'conv-long', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Reload page await page.reload(); await waitForAppReady(page); // Verify all messages loaded const state = await storeInspectors.getChatState<{ messages: Array<{ id: string }>; }>(page); expect(state?.messages?.length).toBe(50); }); test('MEM-CROSS-05: Memory survives browser close simulation', async ({ page }) => { // Create important data const importantData = { messages: [ { id: 'important-1', role: 'user', content: 'Important information to remember', timestamp: new Date() } as any, ], conversations: [{ id: 'important-conv', title: 'Important Conversation', messages: [{ id: 'important-1', role: 'user', content: 'Important information to remember', timestamp: new Date() } as any], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'important-conv', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }; await storeInspectors.setChatState(page, importantData); // Clear in-memory state but keep localStorage await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.setState({ messages: [], conversations: [] }); } }); // Trigger rehydration by reloading await page.reload(); await waitForAppReady(page); // Verify data restored const state = await storeInspectors.getChatState<{ conversations: Array<{ title: string }>; }>(page); expect(state?.conversations?.length).toBeGreaterThan(0); expect(state?.conversations?.[0]?.title).toContain('Important'); }); }); // ============================================ // Test Suite 3: Context Compression Tests // ============================================ test.describe('Memory System - Context Compression Tests', () => { test.describe.configure({ mode: 'parallel' }); test.beforeEach(async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); }); test('MEM-COMP-01: Large context triggers compression check', async ({ page }) => { // Create conversation with many messages to approach token limit const messages = []; for (let i = 0; i < 100; i++) { messages.push({ id: `msg-${i}`, role: i % 2 === 0 ? 'user' : 'assistant', content: `This is a longer message number ${i} that contains enough text to contribute to token count. It has multiple sentences and should be considered during context compression checks.`, timestamp: new Date(Date.now() + i * 1000), }); } await storeInspectors.setChatState(page, { messages: messages as any[], conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Check if context compactor is available const compactorCheck = await page.evaluate(async () => { try { // Check for context compactor const compactor = (window as any).__CONTEXT_COMPACTOR__; if (compactor) { const messages = Array.from({ length: 100 }, (_, i) => ({ role: i % 2 === 0 ? 'user' : 'assistant', content: `Message ${i}`, })); const result = compactor.checkThreshold(messages); return { available: true, result }; } return { available: false }; } catch (e) { return { available: false, error: String(e) }; } }); // If compactor is available, verify it works if (compactorCheck.available) { expect(compactorCheck.result).toHaveProperty('shouldCompact'); expect(compactorCheck.result).toHaveProperty('currentTokens'); } }); test('MEM-COMP-02: Compression preserves key information', async ({ page }) => { // Create conversation with key information const keyMessages = [ { id: 'key-1', role: 'user', content: 'My name is Alice and I prefer Python', timestamp: new Date() } as any, { id: 'key-2', role: 'assistant', content: 'Nice to meet you Alice! I will remember your Python preference.', timestamp: new Date() } as any, { id: 'key-3', role: 'user', content: 'I work on machine learning projects', timestamp: new Date() } as any, { id: 'key-4', role: 'assistant', content: 'Great! Machine learning with Python is a powerful combination.', timestamp: new Date() } as any, ]; await storeInspectors.setChatState(page, { messages: keyMessages, conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Verify key information is preserved const state = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; }>(page); const allContent = state?.messages?.map(m => m.content).join(' ') || ''; expect(allContent).toContain('Alice'); expect(allContent).toContain('Python'); }); test('MEM-COMP-03: Context window limits respected', async ({ page }) => { // Get context window limit from store/model const limitCheck = await page.evaluate(async () => { try { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { const model = stores.chat.getState().currentModel; // Different models have different context windows const contextLimits: Record = { 'claude-sonnet-4-20250514': 200000, 'claude-3-haiku-20240307': 200000, 'claude-3-opus-20240229': 200000, 'gpt-4o': 128000, }; return { model, limit: contextLimits[model] || 200000, }; } return { model: null, limit: null }; } catch (e) { return { error: String(e) }; } }); // Verify limit is reasonable if (limitCheck.limit) { expect(limitCheck.limit).toBeGreaterThan(0); expect(limitCheck.limit).toBeLessThanOrEqual(200000); } }); test('MEM-COMP-04: Summarization creates compact representation', async ({ page }) => { // Create conversation that would benefit from summarization const longConversation = []; for (let i = 0; i < 30; i++) { longConversation.push({ id: `sum-msg-${i}`, role: i % 2 === 0 ? 'user' : 'assistant', content: `Topic discussion part ${i}: We are discussing the implementation of a feature that requires careful consideration of various factors including performance, maintainability, and user experience.`, timestamp: new Date(Date.now() + i * 60000), }); } await storeInspectors.setChatState(page, { messages: longConversation as any[], conversations: [{ id: 'conv-sum', title: 'Long Discussion', messages: longConversation as any[], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'conv-sum', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Verify conversation stored const state = await storeInspectors.getChatState<{ messages: Array<{ id: string }>; }>(page); expect(state?.messages?.length).toBe(30); }); }); // ============================================ // Test Suite 4: Memory Extraction Tests // ============================================ test.describe('Memory System - Memory Extraction Tests', () => { test.describe.configure({ mode: 'parallel' }); test.beforeEach(async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); }); test('MEM-EXTRACT-01: Extract user preferences from conversation', async ({ page }) => { // Create conversation with clear preferences const preferenceMessages = [ { id: 'pref-1', role: 'user', content: 'I prefer using React over Vue for frontend', timestamp: new Date() } as any, { id: 'pref-2', role: 'assistant', content: 'I will use React for frontend tasks.', timestamp: new Date() } as any, { id: 'pref-3', role: 'user', content: 'Please use TypeScript for all code', timestamp: new Date() } as any, { id: 'pref-4', role: 'assistant', content: 'TypeScript it is!', timestamp: new Date() } as any, ]; await storeInspectors.setChatState(page, { messages: preferenceMessages, conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Check memory extraction availability const extractionCheck = await page.evaluate(async () => { try { const extractor = (window as any).__MEMORY_EXTRACTOR__; if (extractor) { return { available: true }; } return { available: false }; } catch (e) { return { available: false, error: String(e) }; } }); // Memory extractor should be available or gracefully handled expect(typeof extractionCheck.available).toBe('boolean'); }); test('MEM-EXTRACT-02: Extract factual information', async ({ page }) => { // Create conversation with facts const factMessages = [ { id: 'fact-1', role: 'user', content: 'The project deadline is December 15th', timestamp: new Date() } as any, { id: 'fact-2', role: 'assistant', content: 'Noted: December 15th deadline.', timestamp: new Date() } as any, { id: 'fact-3', role: 'user', content: 'The team consists of 5 developers', timestamp: new Date() } as any, { id: 'fact-4', role: 'assistant', content: 'Got it, 5 developers on the team.', timestamp: new Date() } as any, ]; await storeInspectors.setChatState(page, { messages: factMessages, conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Verify facts in conversation const state = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; }>(page); const allContent = state?.messages?.map(m => m.content).join(' ') || ''; expect(allContent).toContain('December 15th'); expect(allContent).toContain('5 developers'); }); test('MEM-EXTRACT-03: Memory importance scoring', async ({ page }) => { // Create conversation with varying importance const messages = [ { id: 'imp-1', role: 'user', content: 'CRITICAL: Do not delete production database', timestamp: new Date() } as any, { id: 'imp-2', role: 'assistant', content: 'Understood, I will never delete production data.', timestamp: new Date() } as any, { id: 'imp-3', role: 'user', content: 'The weather is nice today', timestamp: new Date() } as any, { id: 'imp-4', role: 'assistant', content: 'Indeed it is!', timestamp: new Date() } as any, ]; await storeInspectors.setChatState(page, { messages, conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Check memory manager const memoryCheck = await page.evaluate(async () => { try { const memoryMgr = (window as any).__MEMORY_MANAGER__; if (memoryMgr) { return { available: true }; } return { available: false }; } catch (e) { return { available: false }; } }); // Memory manager should be available or gracefully handled expect(typeof memoryCheck.available).toBe('boolean'); }); test('MEM-EXTRACT-04: Memory search retrieves relevant info', async ({ page }) => { // Create conversation with searchable content const searchableMessages = [ { id: 'search-1', role: 'user', content: 'The API key is sk-test-12345', timestamp: new Date() } as any, { id: 'search-2', role: 'assistant', content: 'API key stored.', timestamp: new Date() } as any, { id: 'search-3', role: 'user', content: 'Database connection string is postgres://localhost:5432/mydb', timestamp: new Date() } as any, { id: 'search-4', role: 'assistant', content: 'Connection string noted.', timestamp: new Date() } as any, ]; await storeInspectors.setChatState(page, { messages: searchableMessages, conversations: [], currentConversationId: null, currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Search for API key in messages const state = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; }>(page); const apiMessage = state?.messages?.find(m => m.content.includes('API key')); expect(apiMessage).toBeDefined(); expect(apiMessage?.content).toContain('sk-test-12345'); }); }); // ============================================ // Test Suite 5: Memory Integration Tests // ============================================ test.describe('Memory System - Integration Tests', () => { test('MEM-INT-01: Full conversation flow with memory', async ({ page }) => { await setupMockGateway(page); await mockAgentMessageResponse(page, 'Memory integration test response'); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // Clear existing state await storeInspectors.clearStore(page, 'CHAT'); await page.waitForTimeout(300); // Create initial conversation const conversation = { id: 'conv-int', title: 'Memory Integration Test', messages: [ { id: 'int-1', role: 'user', content: 'Remember my preference: I use VS Code', timestamp: new Date() } as any, { id: 'int-2', role: 'assistant', content: 'I will remember you use VS Code.', timestamp: new Date() } as any, ], sessionKey: 'session-int', agentId: null, createdAt: new Date(), updatedAt: new Date(), }; await storeInspectors.setChatState(page, { messages: conversation.messages, conversations: [conversation], currentConversationId: 'conv-int', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: 'session-int', agents: [], }); // Reload to test persistence await page.reload(); await waitForAppReady(page); // Verify conversation restored const state = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; conversations: Array<{ title: string }>; }>(page); expect(state?.messages?.length).toBe(2); expect(state?.conversations?.length).toBe(1); expect(state?.conversations?.[0]?.title).toBe('Memory Integration Test'); }); test('MEM-INT-02: Memory survives multiple navigations', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // Create test data const testContent = `Navigation test ${generateId()}`; await storeInspectors.setChatState(page, { messages: [ { id: 'nav-1', role: 'user', content: testContent, timestamp: new Date() } as any, ], conversations: [{ id: 'conv-nav', title: 'Navigation Test', messages: [{ id: 'nav-1', role: 'user', content: testContent, timestamp: new Date() } as any], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'conv-nav', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Navigate to different tabs await navigateToTab(page, 'Hands'); await page.waitForTimeout(500); await navigateToTab(page, '工作流'); await page.waitForTimeout(500); await navigateToTab(page, '技能'); await page.waitForTimeout(500); // Navigate back to chat await navigateToTab(page, '分身'); await page.waitForTimeout(500); // Verify memory persisted const state = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; }>(page); expect(state?.messages?.[0]?.content).toBe(testContent); }); test('MEM-INT-03: Memory with multiple agents', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // Set up conversations with different agents const conversations = [ { id: 'conv-dev', title: 'Dev Agent Conversation', messages: [ { id: 'dev-1', role: 'user', content: 'Help me write code', timestamp: new Date() } as any, { id: 'dev-2', role: 'assistant', content: 'Sure, what code?', timestamp: new Date() } as any, ], sessionKey: null, agentId: 'agent-dev', createdAt: new Date(), updatedAt: new Date(), }, { id: 'conv-qa', title: 'QA Agent Conversation', messages: [ { id: 'qa-1', role: 'user', content: 'Review this code', timestamp: new Date() } as any, { id: 'qa-2', role: 'assistant', content: 'Let me review it', timestamp: new Date() } as any, ], sessionKey: null, agentId: 'agent-qa', createdAt: new Date(), updatedAt: new Date(), }, ]; await storeInspectors.setChatState(page, { messages: [], conversations, currentConversationId: 'conv-dev', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Switch between agent conversations const switchResult = await page.evaluate(async () => { try { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.getState().switchConversation('conv-qa'); return { success: true, currentId: stores.chat.getState().currentConversationId, messagesCount: stores.chat.getState().messages.length, }; } return { success: false }; } catch (e) { return { success: false, error: String(e) }; } }); expect(switchResult.success).toBe(true); expect(switchResult.currentId).toBe('conv-qa'); expect(switchResult.messagesCount).toBe(2); }); test('MEM-INT-04: Error recovery preserves memory', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // Create important conversation const importantContent = `Critical data ${generateId()}`; await storeInspectors.setChatState(page, { messages: [ { id: 'err-1', role: 'user', content: importantContent, timestamp: new Date() } as any, ], conversations: [{ id: 'conv-err', title: 'Error Recovery Test', messages: [{ id: 'err-1', role: 'user', content: importantContent, timestamp: new Date() } as any], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'conv-err', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Simulate error state await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.setState({ isStreaming: true, error: 'Simulated error' }); } }); // Clear error state await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.setState({ isStreaming: false, error: null }); } }); // Verify data still present const state = await storeInspectors.getChatState<{ messages: Array<{ content: string }>; }>(page); expect(state?.messages?.[0]?.content).toBe(importantContent); }); test('MEM-INT-05: Memory cleanup on explicit delete', async ({ page }) => { await setupMockGateway(page); await skipOnboarding(page); await page.goto(BASE_URL); await waitForAppReady(page); // Create conversation to delete await storeInspectors.setChatState(page, { messages: [ { id: 'del-1', role: 'user', content: 'To be deleted', timestamp: new Date() } as any, ], conversations: [{ id: 'conv-del', title: 'To Delete', messages: [{ id: 'del-1', role: 'user', content: 'To be deleted', timestamp: new Date() } as any], sessionKey: null, agentId: null, createdAt: new Date(), updatedAt: new Date(), }], currentConversationId: 'conv-del', currentAgent: null, isStreaming: false, currentModel: 'claude-sonnet-4-20250514', sessionKey: null, agents: [], }); // Delete conversation await page.evaluate(async () => { const stores = (window as any).__ZCLAW_STORES__; if (stores?.chat) { stores.chat.getState().deleteConversation('conv-del'); } }); // Verify deletion const state = await storeInspectors.getChatState<{ conversations: Array<{ id: string }>; }>(page); expect(state?.conversations?.find(c => c.id === 'conv-del')).toBeUndefined(); }); }); // ============================================ // Test Report // ============================================ test.afterAll(async ({}, testInfo) => { console.log('\n========================================'); console.log('ZCLAW Memory System E2E Tests Complete'); console.log('========================================'); console.log(`Test Time: ${new Date().toISOString()}`); console.log('========================================\n'); });