Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- 创建 types.ts 定义完整的类型系统 - 重写 DocumentRenderer.tsx 修复语法错误 - 重写 QuizRenderer.tsx 修复语法错误 - 重写 PresentationContainer.tsx 添加类型守卫 - 重写 TypeSwitcher.tsx 修复类型引用 - 更新 index.ts 移除不存在的 ChartRenderer 导出 审计结果: - 类型检查: 通过 - 单元测试: 222 passed - 构建: 成功
413 lines
12 KiB
Rust
413 lines
12 KiB
Rust
//! 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<Config> = 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);
|
|
}
|