From d95fda3b76908f91726e125753fc5b508565baa8 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 21 Apr 2026 10:56:05 +0800 Subject: [PATCH] =?UTF-8?q?test(growth):=20=E8=BF=9B=E5=8C=96=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=20=E2=80=94=206?= =?UTF-8?q?=20=E4=B8=AA=20E2E=20=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 验证自学习闭环完整链路: - 4 次经验累积 → reuse_count=3 → 模式识别触发 - 低于阈值不触发 → 正确过滤 - 多模式独立跟踪 → 行业上下文保留 - SkillGenerator prompt 构建 → 包含步骤/工具/行业 - QualityGate 验证 → 通过/冲突检测 - FeedbackCollector 信任度 → 正负反馈计分 全量测试: 918 PASS, 0 FAIL --- .../zclaw-growth/tests/evolution_loop_test.rs | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 crates/zclaw-growth/tests/evolution_loop_test.rs diff --git a/crates/zclaw-growth/tests/evolution_loop_test.rs b/crates/zclaw-growth/tests/evolution_loop_test.rs new file mode 100644 index 0000000..def178b --- /dev/null +++ b/crates/zclaw-growth/tests/evolution_loop_test.rs @@ -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); +}