//! 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); }