Files
zclaw_openfang/desktop/tests/e2e/specs/memory.spec.ts
iven c5d91cf9f0 feat: add integration test framework and health check improvements
- Add test helper library with assertion functions (scripts/lib/test-helpers.sh)
- Add gateway integration test script (scripts/tests/gateway-test.sh)
- Add configuration validation tool (scripts/validate-config.ts)
- Add health-check.ts library with Tauri command wrappers
- Add HealthStatusIndicator component to ConnectionStatus.tsx
- Add E2E test specs for memory, settings, and team collaboration
- Update ZCLAW-DEEP-ANALYSIS.md to reflect actual project state

Key improvements:
- Store architecture now properly documented as migrated
- Tauri backend shown as 85-90% complete
- Component integration status clarified

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 00:09:47 +08:00

1137 lines
36 KiB
TypeScript

/**
* 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<string, number> = {
'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');
});