- 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>
1137 lines
36 KiB
TypeScript
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');
|
|
});
|