Files
zclaw_openfang/crates/zclaw-growth/tests/evolution_loop_test.rs
iven 5b5491a08f
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
feat(growth,kernel,runtime): Embedding 接通 + 自学习自动化 — A线+B线 6 项实现
A线 Embedding 接通:
- A1: MemoryRetriever.set_embedding_client() + GrowthIntegration.configure_embedding()
  + Kernel.set_embedding_client() + viking_configure_embedding 传播到 Kernel
- A2: Skill 路由替换 new_tf_idf_only() 为 EmbeddingAdapter + LlmSkillFallback

B线 自学习自动化:
- B1: evolution_bridge.rs — candidate_to_manifest() (PromptOnly, disabled by default)
- B2: Kernel::generate_and_register_skill() 全链路 (LLM→parse→QualityGate→manifest→persist)
- B3: EvolutionMiddleware 双模式 (auto_mode 跳过注入, 留给 kernel 自动处理)
- B4: QualityGate 加固 (body ≥100字符 + 必须含标题 + 置信度上限 1.0)

验证: 934 tests PASS, 0 failures
2026-04-21 15:21:03 +08:00

208 lines
8.2 KiB
Rust

//! Evolution loop integration test
//!
//! Tests the complete self-learning loop:
//! Experience accumulation → Pattern recognition → Evolution suggestion
use std::sync::Arc;
use zclaw_growth::{
EvolutionEngine, Experience, ExperienceStore, PatternAggregator,
SqliteStorage, VikingAdapter,
};
fn make_experience(agent_id: &str, pattern: &str, steps: Vec<&str>, tool: Option<&str>) -> Experience {
let mut exp = Experience::new(
agent_id,
pattern,
&format!("{}相关任务", pattern),
steps.into_iter().map(|s| s.to_string()).collect(),
"成功解决",
);
exp.tool_used = tool.map(|t| t.to_string());
exp
}
/// Store N experiences with the same pain pattern, then verify pattern recognition
#[tokio::test]
async fn test_evolution_loop_four_experiences_trigger_pattern() {
let storage = Arc::new(SqliteStorage::in_memory().await);
let adapter = Arc::new(VikingAdapter::new(storage));
let store = Arc::new(ExperienceStore::new(adapter.clone()));
let agent_id = "test-agent-evolution";
// Store 4 experiences with the same pain pattern
for _ in 0..4 {
let exp = make_experience(
agent_id,
"生成每日报表",
vec!["打开Excel", "选择模板", "导出PDF"],
Some("excel_tool"),
);
store.store_experience(&exp).await.unwrap();
}
// Verify experiences were stored and reuse_count accumulated
let all = store.find_by_agent(agent_id).await.unwrap();
assert_eq!(all.len(), 1, "Same pattern should merge into 1 experience");
assert_eq!(all[0].reuse_count, 3, "4 stores → reuse_count=3");
// Pattern aggregator should find this as evolvable
let agg_store = ExperienceStore::new(adapter.clone());
let aggregator = PatternAggregator::new(agg_store);
let patterns = aggregator.find_evolvable_patterns(agent_id, 3).await.unwrap();
assert_eq!(patterns.len(), 1, "Should find 1 evolvable pattern");
assert_eq!(patterns[0].pain_pattern, "生成每日报表");
assert!(patterns[0].total_reuse >= 3);
assert!(!patterns[0].common_steps.is_empty(), "Should find common steps");
// Evolution engine should detect the same patterns
let engine = EvolutionEngine::new(adapter);
let evolvable = engine.check_evolvable_patterns(agent_id).await.unwrap();
assert_eq!(evolvable.len(), 1, "EvolutionEngine should detect 1 evolvable pattern");
assert_eq!(evolvable[0].pain_pattern, "生成每日报表");
}
/// Verify that experiences below threshold are NOT marked evolvable
#[tokio::test]
async fn test_evolution_loop_below_threshold_not_evolvable() {
let storage = Arc::new(SqliteStorage::in_memory().await);
let adapter = Arc::new(VikingAdapter::new(storage));
let store = Arc::new(ExperienceStore::new(adapter.clone()));
let agent_id = "test-agent-below";
// Store only 2 experiences (below min_reuse=3)
for _ in 0..2 {
let exp = make_experience(agent_id, "低频任务", vec!["步骤1"], None);
store.store_experience(&exp).await.unwrap();
}
let all = store.find_by_agent(agent_id).await.unwrap();
assert_eq!(all.len(), 1);
assert_eq!(all[0].reuse_count, 1, "2 stores → reuse_count=1");
let engine = EvolutionEngine::new(adapter);
let evolvable = engine.check_evolvable_patterns(agent_id).await.unwrap();
assert!(evolvable.is_empty(), "Below threshold should not be evolvable");
}
/// Verify multiple different patterns are tracked independently
#[tokio::test]
async fn test_evolution_loop_multiple_patterns() {
let storage = Arc::new(SqliteStorage::in_memory().await);
let adapter = Arc::new(VikingAdapter::new(storage));
let store = Arc::new(ExperienceStore::new(adapter.clone()));
let agent_id = "test-agent-multi";
// Pattern A: 4 occurrences → evolvable
for _ in 0..4 {
let mut exp = make_experience(agent_id, "报表生成", vec!["打开系统", "选择日期"], Some("browser"));
exp.industry_context = Some("医疗".into());
store.store_experience(&exp).await.unwrap();
}
// Pattern B: 2 occurrences → not evolvable
for _ in 0..2 {
let exp = make_experience(agent_id, "会议纪要", vec!["录音转文字"], None);
store.store_experience(&exp).await.unwrap();
}
let engine = EvolutionEngine::new(adapter);
let evolvable = engine.check_evolvable_patterns(agent_id).await.unwrap();
assert_eq!(evolvable.len(), 1, "Only pattern A should be evolvable");
assert_eq!(evolvable[0].pain_pattern, "报表生成");
assert_eq!(evolvable[0].total_reuse, 3);
assert_eq!(evolvable[0].industry_context, Some("医疗".into()));
}
/// Test SkillGenerator prompt building from evolvable pattern
#[tokio::test]
async fn test_skill_generator_from_evolvable_pattern() {
use zclaw_growth::{AggregatedPattern, SkillGenerator};
let pattern = AggregatedPattern {
pain_pattern: "生成每日报表".to_string(),
experiences: vec![],
common_steps: vec!["打开Excel".into(), "选择模板".into(), "导出PDF".into()],
total_reuse: 5,
tools_used: vec!["excel_tool".into()],
industry_context: Some("医疗".into()),
};
let prompt = SkillGenerator::build_prompt(&pattern);
assert!(prompt.contains("生成每日报表"));
assert!(prompt.contains("打开Excel"));
assert!(prompt.contains("excel_tool"));
}
/// Test QualityGate validates skill candidates
#[tokio::test]
async fn test_quality_gate_validation() {
use zclaw_growth::{QualityGate, SkillCandidate};
let candidate = SkillCandidate {
name: "每日报表生成".to_string(),
description: "自动生成并导出每日报表".to_string(),
triggers: vec!["生成报表".into(), "每日报表".into()],
tools: vec!["excel_tool".into()],
body_markdown: "# 每日报表生成\n\n## 步骤一:数据收集\n从数据库查询昨日所有交易记录和运营数据。\n\n## 步骤二:数据整理\n将原始数据按部门、类型进行分类汇总。\n\n## 步骤三:报表输出\n生成标准化报表并导出为PDF格式。".to_string(),
source_pattern: "生成每日报表".to_string(),
confidence: 0.85,
version: 1,
};
let gate = QualityGate::new(0.7, vec![]);
let report = gate.validate_skill(&candidate);
assert!(report.passed, "Valid candidate should pass quality gate");
assert!(report.issues.is_empty());
// Test with conflicting trigger
let gate_with_conflict = QualityGate::new(0.7, vec!["生成报表".into()]);
let report = gate_with_conflict.validate_skill(&candidate);
assert!(!report.passed, "Conflicting trigger should fail");
}
/// Test FeedbackCollector trust score updates
#[tokio::test]
async fn test_feedback_collector_trust_evolution() {
use zclaw_growth::feedback_collector::{
EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, Sentiment,
};
let storage = Arc::new(SqliteStorage::in_memory().await);
let adapter = Arc::new(VikingAdapter::new(storage));
let mut collector = FeedbackCollector::with_viking(adapter);
// Submit 3 positive feedbacks across 2 skills
for i in 0..3 {
let entry = FeedbackEntry {
artifact_id: format!("skill-{}", i % 2),
artifact_type: EvolutionArtifact::Skill,
signal: FeedbackSignal::Explicit,
sentiment: Sentiment::Positive,
details: Some("很有用".into()),
timestamp: chrono::Utc::now(),
};
collector.submit_feedback(entry);
}
// Submit 1 negative feedback
let negative = FeedbackEntry {
artifact_id: "skill-0".to_string(),
artifact_type: EvolutionArtifact::Skill,
signal: FeedbackSignal::Explicit,
sentiment: Sentiment::Negative,
details: Some("步骤有误".into()),
timestamp: chrono::Utc::now(),
};
collector.submit_feedback(negative);
// skill-0: 2 positive + 1 negative
let trust0 = collector.get_trust("skill-0").unwrap();
assert_eq!(trust0.positive_count, 2);
assert_eq!(trust0.negative_count, 1);
// skill-1: 1 positive only
let trust1 = collector.get_trust("skill-1").unwrap();
assert_eq!(trust1.positive_count, 1);
assert_eq!(trust1.negative_count, 0);
}