Compare commits
2 Commits
f11ac6e434
...
ec22f0f357
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec22f0f357 | ||
|
|
d95fda3b76 |
207
crates/zclaw-growth/tests/evolution_loop_test.rs
Normal file
207
crates/zclaw-growth/tests/evolution_loop_test.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
//! 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\n1. 打开Excel\n2. 选择模板\n3. 导出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);
|
||||
}
|
||||
@@ -25,6 +25,15 @@ tags: [log, history]
|
||||
|
||||
验证: cargo check 0 error | cargo test 912 PASS | tsc --noEmit 0 error
|
||||
|
||||
## [2026-04-21] test | Phase 2 自学习闭环验证 — 进化引擎全链路确认 (commit d95fda3)
|
||||
|
||||
**审计发现**: 进化引擎管道已完整连接,Phase 0 的 reuse_count 修复是唯一缺失拼图。
|
||||
- 经验累积 → PatternAggregator(>=3) → EvolutionEngine → EvolutionMiddleware(@78) → <evolution-suggestion> 注入
|
||||
- L2 SkillGenerator + QualityGate 代码完整,标记 @reserved 用于全自动化(后续)
|
||||
- 当前"人在环"模式:检测到可进化模式 → 提示用户确认 → 不自动生成
|
||||
|
||||
**6 个集成测试**: 经验累积触发/阈值过滤/多模式独立/SkillGenerator prompt/QualityGate验证/FeedbackCollector计分。全量 918 PASS。
|
||||
|
||||
## [2026-04-20] fix | 50 轮功能链路审计 7 项断链修复 (commit f291736)
|
||||
|
||||
**审计报告**: `docs/test-evidence/FUNCTIONAL_CHAIN_AUDIT_2026_04_20.md`
|
||||
|
||||
Reference in New Issue
Block a user