//! M3.7.4 — Context Endpoint Integration Tests //! //! Tests three-tier retrieval, budget management, and graceful degradation. use mem_cli::context_endpoint::{ ContextLookup, ContextRequest, ContextResponse, TieredLesson, SkillRecommendation, }; #[tokio::test] async fn test_a1_tier1_exact_match() { // Tier 1: Exact signature match let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("github-actions".to_string()), task: None, signature_source: Some("ERESOLVE unable to resolve dependency tree".to_string()), project: Some("test".to_string()), scope: Some("project".to_string()), budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); let resp = result.unwrap(); assert_eq!(resp.budget.limit, 6000); } #[tokio::test] async fn test_a2_tier1_counts_occurrence() { // Calling twice should track occurrence count let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req1 = ContextRequest { tool: Some("npm".to_string()), task: None, signature_source: Some("peer dep conflict".to_string()), project: Some("test".to_string()), scope: None, budget: None, }; let result1 = lookup.lookup(req1).await; assert!(result1.is_ok()); let req2 = ContextRequest { tool: Some("npm".to_string()), task: None, signature_source: Some("peer dep conflict".to_string()), project: Some("test".to_string()), scope: None, budget: None, }; let result2 = lookup.lookup(req2).await; assert!(result2.is_ok()); } #[tokio::test] async fn test_a3_tier2_symptom_search() { // Tier 2: Vector search on symptoms let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("docker".to_string()), task: None, signature_source: Some("connection timeout connecting to Docker daemon".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); let resp = result.unwrap(); assert_eq!(resp.budget.limit, 6000); } #[tokio::test] async fn test_a4_tier2_beats_text_only() { // Tier 2 with symptoms should rank higher than text-only let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("kubernetes".to_string()), task: None, signature_source: Some("pod CrashLoopBackOff".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); } #[tokio::test] async fn test_a5_tier3_fallback() { // Tier 3: Reference corpus fallback for unknown failures let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("unknown-tool".to_string()), task: None, signature_source: Some("some random error we have never seen".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); } #[tokio::test] async fn test_a6_tier_precedence_over_score() { // Tier 1 should lead even if tier 3 scores higher let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("github-actions".to_string()), task: None, signature_source: Some("ERESOLVE dependency conflict".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); let resp = result.unwrap(); // Tier 1 results should come first in the response if !resp.lessons.is_empty() { assert_eq!(resp.lessons[0].tier, 1); } } #[tokio::test] async fn test_a7_superseded_excluded() { // Superseded memories should be excluded let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("test-tool".to_string()), task: None, signature_source: Some("test error".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); } #[tokio::test] async fn test_a8_budget_management() { // Test budget drops in order: R, then tier 2, then skills let lookup = ContextLookup::new(100, "test".to_string(), "project".to_string()); // Small budget let req = ContextRequest { tool: Some("test".to_string()), task: None, signature_source: Some("test".to_string()), project: Some("test".to_string()), scope: None, budget: Some(100), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); let resp = result.unwrap(); assert!(resp.budget.used <= resp.budget.limit); } #[tokio::test] async fn test_a9_skills_degrade() { // Skills timeout should return 200 with degraded flag let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("test".to_string()), task: None, signature_source: Some("test".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); let resp = result.unwrap(); // Should return 200 even if skills fails assert_eq!(resp.budget.limit, 6000); } #[tokio::test] async fn test_a10_signature_without_tool() { // Should infer tool from signature if not provided let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: None, task: None, signature_source: Some("npm ERR! code ERESOLVE".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); } #[tokio::test] async fn test_a11_scope_federation() { // scope: all-projects should find signatures across projects let lookup = ContextLookup::new(6000, "test".to_string(), "all-projects".to_string()); let req = ContextRequest { tool: Some("test".to_string()), task: None, signature_source: Some("common error".to_string()), project: None, scope: Some("all-projects".to_string()), budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); } #[tokio::test] async fn test_a12_budget_calculation() { // budget.used should match actual content let lookup = ContextLookup::new(6000, "test".to_string(), "project".to_string()); let req = ContextRequest { tool: Some("test".to_string()), task: None, signature_source: Some("error".to_string()), project: Some("test".to_string()), scope: None, budget: Some(6000), }; let result = lookup.lookup(req).await; assert!(result.is_ok()); let resp = result.unwrap(); assert!(resp.budget.used >= 0); assert!(resp.budget.used <= resp.budget.limit); } #[test] fn test_tiered_lesson_structure() { let lesson = TieredLesson { tier: 1, level: "L1".to_string(), score: None, seen_count: Some(3), last_seen: Some("2024-01-15".to_string()), matched_kind: None, text: "npm ci --legacy-peer-deps".to_string(), parents: None, }; assert_eq!(lesson.tier, 1); assert_eq!(lesson.seen_count, Some(3)); assert_eq!(lesson.level, "L1"); } #[test] fn test_context_response_serialization() { let resp = ContextResponse { tier: 1, lessons: vec![], skills: vec![], budget: mem_cli::context_endpoint::BudgetInfo { limit: 6000, used: 0, dropped: vec![], }, degraded: None, }; let json = serde_json::to_string(&resp).unwrap(); assert!(json.contains("\"tier\":1")); assert!(json.contains("\"limit\":6000")); } #[test] fn test_skill_recommendation_structure() { let skill = SkillRecommendation { name: "ci-triage".to_string(), score: 0.77, description: Some("CI troubleshooting".to_string()), }; assert_eq!(skill.name, "ci-triage"); assert!(skill.score > 0.7 && skill.score < 1.0); }