//! Integration tests for ZCLAW Growth System //! //! Tests the complete flow: store → find → inject use std::sync::Arc; use zclaw_growth::{ FindOptions, MemoryEntry, MemoryRetriever, MemoryType, PromptInjector, RetrievalConfig, RetrievalResult, SqliteStorage, VikingAdapter, }; use zclaw_types::AgentId; /// Test complete memory lifecycle #[tokio::test] async fn test_memory_lifecycle() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage)); // Create agent ID and use its string form for storage let agent_id = AgentId::new(); let agent_str = agent_id.to_string(); // 1. Store a preference let pref = MemoryEntry::new( &agent_str, MemoryType::Preference, "communication-style", "用户偏好简洁的回复,不喜欢冗长的解释".to_string(), ) .with_keywords(vec!["简洁".to_string(), "沟通风格".to_string()]) .with_importance(8); adapter.store(&pref).await.unwrap(); // 2. Store knowledge let knowledge = MemoryEntry::new( &agent_str, MemoryType::Knowledge, "rust-expertise", "用户是 Rust 开发者,熟悉 async/await 和 trait 系统".to_string(), ) .with_keywords(vec!["Rust".to_string(), "开发者".to_string()]); adapter.store(&knowledge).await.unwrap(); // 3. Store experience let experience = MemoryEntry::new( &agent_str, MemoryType::Experience, "browser-skill", "浏览器技能在搜索技术文档时效果很好".to_string(), ) .with_keywords(vec!["浏览器".to_string(), "技能".to_string()]); adapter.store(&experience).await.unwrap(); // 4. Retrieve memories - directly from adapter first let direct_results = adapter .find( "Rust", FindOptions { scope: Some(format!("agent://{}", agent_str)), limit: Some(10), min_similarity: Some(0.1), }, ) .await .unwrap(); println!("Direct find results: {:?}", direct_results.len()); let retriever = MemoryRetriever::new(adapter.clone()); // Use lower similarity threshold for testing let config = RetrievalConfig { min_similarity: 0.1, ..RetrievalConfig::default() }; let retriever = retriever.with_config(config); let result = retriever .retrieve(&agent_id, "Rust 编程") .await .unwrap(); println!("Knowledge results: {:?}", result.knowledge.len()); println!("Preferences results: {:?}", result.preferences.len()); println!("Experience results: {:?}", result.experience.len()); // Should find the knowledge entry assert!(!result.knowledge.is_empty(), "Expected to find knowledge entries but found none. Direct results: {}", direct_results.len()); assert!(result.knowledge[0].content.contains("Rust")); // 5. Inject into prompt let injector = PromptInjector::new(); let base_prompt = "你是一个有帮助的 AI 助手。"; let enhanced = injector.inject_with_format(base_prompt, &result); // Enhanced prompt should contain memory context assert!(enhanced.len() > base_prompt.len()); } /// Test semantic search ranking #[tokio::test] async fn test_semantic_search_ranking() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage.clone())); // Store multiple entries with different relevance let entries = vec![ MemoryEntry::new( "agent-1", MemoryType::Knowledge, "rust-basics", "Rust 是一门系统编程语言,注重安全性和性能".to_string(), ) .with_keywords(vec!["Rust".to_string(), "系统编程".to_string()]), MemoryEntry::new( "agent-1", MemoryType::Knowledge, "python-basics", "Python 是一门高级编程语言,易于学习".to_string(), ) .with_keywords(vec!["Python".to_string(), "高级语言".to_string()]), MemoryEntry::new( "agent-1", MemoryType::Knowledge, "rust-async", "Rust 的 async/await 语法用于异步编程".to_string(), ) .with_keywords(vec!["Rust".to_string(), "async".to_string(), "异步".to_string()]), ]; for entry in &entries { adapter.store(entry).await.unwrap(); } // Search for "Rust 异步编程" let results = adapter .find( "Rust 异步编程", FindOptions { scope: Some("agent://agent-1".to_string()), limit: Some(10), min_similarity: Some(0.1), }, ) .await .unwrap(); // Rust async entry should rank highest assert!(!results.is_empty()); assert!(results[0].content.contains("async") || results[0].content.contains("Rust")); } /// Test memory importance and access count #[tokio::test] async fn test_importance_and_access() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage.clone())); // Create entries with different importance let high_importance = MemoryEntry::new( "agent-1", MemoryType::Preference, "critical", "这是非常重要的偏好".to_string(), ) .with_importance(10); let low_importance = MemoryEntry::new( "agent-1", MemoryType::Preference, "minor", "这是不太重要的偏好".to_string(), ) .with_importance(2); adapter.store(&high_importance).await.unwrap(); adapter.store(&low_importance).await.unwrap(); // Access the low importance one multiple times for _ in 0..5 { let _ = adapter.get(&low_importance.uri).await; } // Search should consider both importance and access count let results = adapter .find( "偏好", FindOptions { scope: Some("agent://agent-1".to_string()), limit: Some(10), min_similarity: None, }, ) .await .unwrap(); assert_eq!(results.len(), 2); } /// Test prompt injection with token budget #[tokio::test] async fn test_prompt_injection_token_budget() { let mut result = RetrievalResult::default(); // Add memories that exceed budget for i in 0..10 { result.preferences.push( MemoryEntry::new( "agent-1", MemoryType::Preference, &format!("pref-{}", i), "这是一个很长的偏好描述,用于测试 token 预算控制功能。".repeat(5), ), ); } result.total_tokens = result.calculate_tokens(); // Budget is 500 tokens by default let injector = PromptInjector::new(); let base = "Base prompt"; let enhanced = injector.inject_with_format(base, &result); // Should include memory context assert!(enhanced.len() > base.len()); } /// Test metadata storage #[tokio::test] async fn test_metadata_operations() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage)); // Store metadata using typed API #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] struct Config { version: String, auto_extract: bool, } let config = Config { version: "1.0.0".to_string(), auto_extract: true, }; adapter.store_metadata("agent-config", &config).await.unwrap(); // Retrieve metadata let retrieved: Option = adapter.get_metadata("agent-config").await.unwrap(); assert!(retrieved.is_some()); let parsed = retrieved.unwrap(); assert_eq!(parsed.version, "1.0.0"); assert_eq!(parsed.auto_extract, true); } /// Test memory deletion and cleanup #[tokio::test] async fn test_memory_deletion() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage)); let entry = MemoryEntry::new( "agent-1", MemoryType::Knowledge, "temp", "Temporary knowledge".to_string(), ); adapter.store(&entry).await.unwrap(); // Verify stored let retrieved = adapter.get(&entry.uri).await.unwrap(); assert!(retrieved.is_some()); // Delete adapter.delete(&entry.uri).await.unwrap(); // Verify deleted let retrieved = adapter.get(&entry.uri).await.unwrap(); assert!(retrieved.is_none()); // Verify not in search results let results = adapter .find( "Temporary", FindOptions { scope: Some("agent://agent-1".to_string()), limit: Some(10), min_similarity: None, }, ) .await .unwrap(); assert!(results.is_empty()); } /// Test cross-agent isolation #[tokio::test] async fn test_agent_isolation() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage)); // Store memories for different agents let agent1_memory = MemoryEntry::new( "agent-1", MemoryType::Knowledge, "secret", "Agent 1 的秘密信息".to_string(), ); let agent2_memory = MemoryEntry::new( "agent-2", MemoryType::Knowledge, "secret", "Agent 2 的秘密信息".to_string(), ); adapter.store(&agent1_memory).await.unwrap(); adapter.store(&agent2_memory).await.unwrap(); // Agent 1 should only see its own memories let results = adapter .find( "秘密", FindOptions { scope: Some("agent://agent-1".to_string()), limit: Some(10), min_similarity: None, }, ) .await .unwrap(); assert_eq!(results.len(), 1); assert!(results[0].content.contains("Agent 1")); // Agent 2 should only see its own memories let results = adapter .find( "秘密", FindOptions { scope: Some("agent://agent-2".to_string()), limit: Some(10), min_similarity: None, }, ) .await .unwrap(); assert_eq!(results.len(), 1); assert!(results[0].content.contains("Agent 2")); } /// Test Chinese text handling #[tokio::test] async fn test_chinese_text_handling() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage)); let entry = MemoryEntry::new( "中文测试", MemoryType::Knowledge, "中文知识", "这是一个中文测试,包含关键词:人工智能、机器学习、深度学习。".to_string(), ) .with_keywords(vec!["人工智能".to_string(), "机器学习".to_string()]); adapter.store(&entry).await.unwrap(); // Search with Chinese query let results = adapter .find( "人工智能", FindOptions { scope: Some("agent://中文测试".to_string()), limit: Some(10), min_similarity: Some(0.1), }, ) .await .unwrap(); assert!(!results.is_empty()); assert!(results[0].content.contains("人工智能")); } /// Test find by prefix #[tokio::test] async fn test_find_by_prefix() { let storage = Arc::new(SqliteStorage::in_memory().await); let adapter = Arc::new(VikingAdapter::new(storage)); // Store multiple entries under same agent for i in 0..5 { let entry = MemoryEntry::new( "agent-1", MemoryType::Knowledge, &format!("topic-{}", i), format!("Content for topic {}", i), ); adapter.store(&entry).await.unwrap(); } // Find all entries for agent-1 let results = adapter .find_by_prefix("agent://agent-1") .await .unwrap(); assert_eq!(results.len(), 5); }