Skip to main content

mockforge_intelligence/ai_studio/
debug_analyzer.rs

1//! AI-guided debugging analyzer
2//!
3//! This module provides functionality to analyze test failures and suggest fixes.
4//! It integrates with the existing failure analysis infrastructure to provide
5//! AI-powered debugging assistance.
6
7use crate::ai_studio::debug_context::DebugContext as UnifiedDebugContext;
8use crate::ai_studio::debug_context_integrator::DebugContextIntegrator;
9use crate::failure_analysis::{
10    context_collector::FailureContextCollector, narrative_generator::FailureNarrativeGenerator,
11    types::FailureContext,
12};
13use crate::intelligent_behavior::llm_client::LlmClient;
14use crate::intelligent_behavior::types::LlmGenerationRequest;
15use crate::intelligent_behavior::IntelligentBehaviorConfig;
16use mockforge_foundation::Result;
17use serde::{Deserialize, Serialize};
18
19/// Debug analyzer for test failure analysis
20pub struct DebugAnalyzer {
21    /// Context collector for gathering failure details
22    context_collector: FailureContextCollector,
23    /// Narrative generator for root cause analysis
24    narrative_generator: FailureNarrativeGenerator,
25    /// LLM client for generating suggestions
26    llm_client: LlmClient,
27    /// Optional debug context integrator for collecting subsystem context
28    context_integrator: Option<DebugContextIntegrator>,
29}
30
31impl DebugAnalyzer {
32    /// Create a new debug analyzer with default configuration
33    pub fn new() -> Self {
34        let config = IntelligentBehaviorConfig::default();
35        Self {
36            context_collector: FailureContextCollector::new(),
37            narrative_generator: FailureNarrativeGenerator::new(config.clone()),
38            llm_client: LlmClient::new(config.behavior_model),
39            context_integrator: None,
40        }
41    }
42
43    /// Create a new debug analyzer with custom configuration
44    pub fn with_config(config: IntelligentBehaviorConfig) -> Self {
45        Self {
46            context_collector: FailureContextCollector::new(),
47            narrative_generator: FailureNarrativeGenerator::new(config.clone()),
48            llm_client: LlmClient::new(config.behavior_model),
49            context_integrator: None,
50        }
51    }
52
53    /// Create a new debug analyzer with context integrator
54    pub fn with_integrator(integrator: DebugContextIntegrator) -> Self {
55        let config = IntelligentBehaviorConfig::default();
56        Self {
57            context_collector: FailureContextCollector::new(),
58            narrative_generator: FailureNarrativeGenerator::new(config.clone()),
59            llm_client: LlmClient::new(config.behavior_model),
60            context_integrator: Some(integrator),
61        }
62    }
63
64    /// Create a new debug analyzer with config and integrator
65    pub fn with_config_and_integrator(
66        config: IntelligentBehaviorConfig,
67        integrator: DebugContextIntegrator,
68    ) -> Self {
69        Self {
70            context_collector: FailureContextCollector::new(),
71            narrative_generator: FailureNarrativeGenerator::new(config.clone()),
72            llm_client: LlmClient::new(config.behavior_model),
73            context_integrator: Some(integrator),
74        }
75    }
76
77    /// Analyze a test failure and suggest fixes
78    ///
79    /// This method analyzes test failure logs and provides:
80    /// - Root cause identification
81    /// - Specific suggestions for fixing the issue
82    /// - Links to related mock configurations (personas, reality settings, contracts)
83    pub async fn analyze(&self, request: &DebugRequest) -> Result<DebugResponse> {
84        // Parse test logs to extract failure information
85        let failure_info = self.parse_test_logs(&request.test_logs)?;
86
87        // Collect failure context
88        let context = self.context_collector.collect_context(
89            &failure_info.method.unwrap_or_else(|| "UNKNOWN".to_string()),
90            &failure_info.path.unwrap_or_else(|| "/".to_string()),
91            failure_info.status_code,
92            failure_info.error_message.clone(),
93        )?;
94
95        // Collect unified debug context from subsystems (if integrator is available)
96        let unified_context = if let Some(ref integrator) = self.context_integrator {
97            Some(integrator.collect_unified_context(request.workspace_id.as_deref()).await?)
98        } else {
99            None
100        };
101
102        // Generate narrative for root cause
103        let narrative = self.narrative_generator.generate_narrative(&context).await?;
104        let root_cause = if narrative.summary.is_empty() {
105            "Unable to determine root cause from provided logs".to_string()
106        } else {
107            narrative.summary.clone()
108        };
109
110        // Generate AI-powered suggestions with unified context
111        let mut suggestions = self
112            .generate_suggestions(&context, &narrative, unified_context.as_ref())
113            .await?;
114
115        // Generate patch operations for suggestions
116        self.generate_patches(&mut suggestions, &context, &narrative, unified_context.as_ref())?;
117
118        // Identify related configurations with unified context
119        let related_configs = self.identify_related_configs(&context, unified_context.as_ref());
120
121        Ok(DebugResponse {
122            root_cause,
123            suggestions,
124            related_configs,
125            context: Some(context),
126            unified_context,
127        })
128    }
129
130    /// Parse test logs to extract failure information
131    fn parse_test_logs(&self, logs: &str) -> Result<ParsedFailureInfo> {
132        // Simple parsing - in a real implementation, this would use more sophisticated
133        // log parsing to extract HTTP methods, paths, status codes, etc.
134        let mut info = ParsedFailureInfo::default();
135
136        // Try to extract HTTP method
137        for method in &["GET", "POST", "PUT", "DELETE", "PATCH"] {
138            if logs.contains(method) {
139                info.method = Some(method.to_string());
140                break;
141            }
142        }
143
144        // Try to extract status code (simple pattern matching)
145        for line in logs.lines() {
146            // Look for 3-digit status codes (400-599 for errors)
147            for word in line.split_whitespace() {
148                if let Ok(status) = word.parse::<u16>() {
149                    if (400..600).contains(&status) {
150                        info.status_code = Some(status);
151                        break;
152                    }
153                }
154            }
155            if info.status_code.is_some() {
156                break;
157            }
158        }
159
160        // Try to extract path (simple pattern matching)
161        for line in logs.lines() {
162            for method in &["GET", "POST", "PUT", "DELETE", "PATCH"] {
163                if let Some(pos) = line.find(method) {
164                    let after_method = &line[pos + method.len()..];
165                    if let Some(path_start) = after_method.find('/') {
166                        let path_part = &after_method[path_start..];
167                        if let Some(path_end) =
168                            path_part.find(|c: char| c.is_whitespace() || c == '?' || c == '\n')
169                        {
170                            info.path = Some(path_part[..path_end].to_string());
171                        } else {
172                            info.path = Some(path_part.trim().to_string());
173                        }
174                        break;
175                    }
176                }
177            }
178            if info.path.is_some() {
179                break;
180            }
181        }
182
183        // Extract error message (look for common error patterns)
184        if logs.contains("error") || logs.contains("Error") || logs.contains("ERROR") {
185            info.error_message = Some(
186                logs.lines()
187                    .find(|line| {
188                        line.to_lowercase().contains("error")
189                            || line.to_lowercase().contains("fail")
190                    })
191                    .unwrap_or("Test failure detected")
192                    .to_string(),
193            );
194        }
195
196        Ok(info)
197    }
198
199    /// Generate AI-powered suggestions for fixing the failure
200    async fn generate_suggestions(
201        &self,
202        context: &FailureContext,
203        narrative: &crate::failure_analysis::types::FailureNarrative,
204        unified_context: Option<&UnifiedDebugContext>,
205    ) -> Result<Vec<DebugSuggestion>> {
206        // Build prompt for suggestion generation
207        let system_prompt = r#"You are an expert at debugging API test failures in mock environments.
208Analyze the failure context and provide specific, actionable suggestions for fixing the issue.
209
210For each suggestion, provide:
2111. A clear title
2122. A detailed description of what to do
2133. A specific action to take
2144. The configuration path to update (if applicable)
2155. Linked artifacts (persona IDs, scenario names, contract paths) that are relevant
216
217Focus on:
218- Contract validation issues (suggest tightening validation or updating contracts)
219- Persona mismatches (suggest adjusting persona traits or reality settings)
220- Mock scenario issues (suggest adding explicit error examples)
221- Reality continuum settings (suggest adjusting reality ratios)
222- Chaos configuration issues (suggest disabling or adjusting chaos rules)
223
224Return your response as a JSON array of suggestions."#;
225
226        // Build unified context summary
227        let unified_summary = if let Some(uc) = unified_context {
228            format!(
229                r#"
230Unified Subsystem Context:
231- Reality Level: {} (chaos: {}, latency: {}ms, MockAI: {})
232- Contract Validation: {} (enforcement: {})
233- Active Scenario: {}
234- Active Persona: {}
235- Chaos Rules: {} active
236"#,
237                uc.reality.level_name.as_deref().unwrap_or("unknown"),
238                uc.reality.chaos_enabled,
239                uc.reality.latency_base_ms,
240                uc.reality.mockai_enabled,
241                uc.contract.validation_enabled,
242                uc.contract.enforcement_mode,
243                uc.scenario.active_scenario.as_deref().unwrap_or("none"),
244                uc.persona.active_persona_id.as_deref().unwrap_or("none"),
245                uc.chaos.active_rules.len()
246            )
247        } else {
248            String::new()
249        };
250
251        let user_prompt = format!(
252            r#"Failure Context:
253- Request: {} {}
254- Status Code: {:?}
255- Error: {:?}
256- Active Chaos Configs: {}
257- Active Consistency Rules: {}
258- Contract Validation: {:?}
259- Behavioral Rules: {}
260
261Narrative Summary: {}
262{}
263
264Provide 3-5 specific suggestions for fixing this test failure. Include linked artifacts (persona IDs, scenario names, contract paths) in your suggestions."#,
265            context.request.method,
266            context.request.path,
267            context.response.as_ref().map(|r| r.status_code),
268            context.error_message,
269            context.chaos_configs.len(),
270            context.consistency_rules.len(),
271            context.contract_validation.is_some(),
272            context.behavioral_rules.len(),
273            if narrative.summary.is_empty() {
274                "No narrative available"
275            } else {
276                &narrative.summary
277            },
278            unified_summary
279        );
280
281        let llm_request = LlmGenerationRequest {
282            system_prompt: system_prompt.to_string(),
283            user_prompt,
284            temperature: 0.3,
285            max_tokens: 1500,
286            schema: None,
287        };
288
289        // Generate suggestions from LLM
290        let response = self.llm_client.generate(&llm_request).await?;
291
292        // Parse suggestions from response
293        let mut suggestions: Vec<DebugSuggestion> = if let Some(suggestions_array) =
294            response.get("suggestions")
295        {
296            serde_json::from_value(suggestions_array.clone()).unwrap_or_else(|_| {
297                // Fallback: create a generic suggestion
298                vec![DebugSuggestion {
299                    title: "Review Mock Configuration".to_string(),
300                    description: "Check your mock configuration for issues related to this failure"
301                        .to_string(),
302                    action: "Review config.yaml and related mock settings".to_string(),
303                    config_path: Some("config.yaml".to_string()),
304                    patch: None,
305                    linked_artifacts: Vec::new(),
306                }]
307            })
308        } else {
309            // Fallback suggestions
310            vec![
311                DebugSuggestion {
312                    title: "Check Contract Validation".to_string(),
313                    description: "The failure may be due to contract validation issues. Review your OpenAPI spec and request/response schemas.".to_string(),
314                    action: "Review contract validation settings".to_string(),
315                    config_path: Some("contract_validation".to_string()),
316                    patch: None,
317                    linked_artifacts: Vec::new(),
318                },
319                DebugSuggestion {
320                    title: "Review Persona Settings".to_string(),
321                    description: "The failure might be related to persona configuration. Check if the active persona matches your test expectations.".to_string(),
322                    action: "Review persona configuration".to_string(),
323                    config_path: Some("consistency.personas".to_string()),
324                    patch: None,
325                    linked_artifacts: Vec::new(),
326                },
327            ]
328        };
329
330        // Enhance suggestions with linked artifacts from unified context
331        if let Some(uc) = unified_context {
332            for suggestion in &mut suggestions {
333                // Add persona link if relevant
334                if suggestion.title.to_lowercase().contains("persona")
335                    || suggestion.description.to_lowercase().contains("persona")
336                {
337                    if let Some(ref persona_id) = uc.persona.active_persona_id {
338                        suggestion.linked_artifacts.push(LinkedArtifact {
339                            artifact_type: "persona".to_string(),
340                            artifact_id: persona_id.to_string(),
341                            artifact_name: uc.persona.active_persona_name.clone(),
342                        });
343                    }
344                }
345
346                // Add scenario link if relevant
347                if suggestion.title.to_lowercase().contains("scenario")
348                    || suggestion.description.to_lowercase().contains("scenario")
349                {
350                    if let Some(ref scenario_id) = uc.scenario.active_scenario {
351                        suggestion.linked_artifacts.push(LinkedArtifact {
352                            artifact_type: "scenario".to_string(),
353                            artifact_id: scenario_id.to_string(),
354                            artifact_name: None,
355                        });
356                    }
357                }
358
359                // Add contract links if relevant
360                if suggestion.title.to_lowercase().contains("contract")
361                    || suggestion.description.to_lowercase().contains("contract")
362                {
363                    for contract_path in &uc.contract.active_contracts {
364                        suggestion.linked_artifacts.push(LinkedArtifact {
365                            artifact_type: "contract".to_string(),
366                            artifact_id: contract_path.to_string(),
367                            artifact_name: None,
368                        });
369                    }
370                }
371
372                // Add reality level link if relevant
373                if suggestion.title.to_lowercase().contains("reality")
374                    || suggestion.description.to_lowercase().contains("reality")
375                {
376                    if let Some(ref level_name) = uc.reality.level_name {
377                        suggestion.linked_artifacts.push(LinkedArtifact {
378                            artifact_type: "reality".to_string(),
379                            artifact_id: uc
380                                .reality
381                                .level
382                                .map(|l| l.value().to_string())
383                                .unwrap_or_default(),
384                            artifact_name: Some(level_name.clone()),
385                        });
386                    }
387                }
388            }
389        }
390
391        Ok(suggestions)
392    }
393
394    /// Generate JSON Patch operations for suggestions
395    fn generate_patches(
396        &self,
397        suggestions: &mut [DebugSuggestion],
398        context: &FailureContext,
399        _narrative: &crate::failure_analysis::types::FailureNarrative,
400        _unified_context: Option<&UnifiedDebugContext>,
401    ) -> Result<()> {
402        for suggestion in suggestions.iter_mut() {
403            // Generate patch based on suggestion type and context
404            if let Some(config_path) = &suggestion.config_path {
405                // Generate appropriate patch based on the suggestion
406                let patch = self.create_patch_for_suggestion(suggestion, config_path, context)?;
407                suggestion.patch = patch;
408            }
409        }
410        Ok(())
411    }
412
413    /// Create a JSON Patch operation for a specific suggestion
414    fn create_patch_for_suggestion(
415        &self,
416        suggestion: &DebugSuggestion,
417        config_path: &str,
418        context: &FailureContext,
419    ) -> Result<Option<DebugPatch>> {
420        // Determine patch operation based on suggestion content
421        let patch = if suggestion.action.contains("add") || suggestion.action.contains("Add") {
422            // Add operation - typically for adding new examples or configurations
423            Some(DebugPatch {
424                op: "add".to_string(),
425                path: self.build_patch_path(config_path, &suggestion.title),
426                value: self.infer_patch_value(suggestion, context),
427                from: None,
428            })
429        } else if suggestion.action.contains("remove") || suggestion.action.contains("Remove") {
430            // Remove operation
431            Some(DebugPatch {
432                op: "remove".to_string(),
433                path: self.build_patch_path(config_path, &suggestion.title),
434                value: None,
435                from: None,
436            })
437        } else {
438            // Replace operation (default)
439            Some(DebugPatch {
440                op: "replace".to_string(),
441                path: self.build_patch_path(config_path, &suggestion.title),
442                value: self.infer_patch_value(suggestion, context),
443                from: None,
444            })
445        };
446
447        Ok(patch)
448    }
449
450    /// Build JSON Pointer path from config path and suggestion context
451    fn build_patch_path(&self, config_path: &str, suggestion_title: &str) -> String {
452        // Convert config path to JSON Pointer format
453        // Example: "consistency.personas" -> "/consistency/personas"
454        // Example: "contract_validation" -> "/contract_validation"
455        let mut path = config_path.replace('.', "/");
456        if !path.starts_with('/') {
457            path = format!("/{}", path);
458        }
459
460        // If suggestion mentions a specific field, append it
461        if suggestion_title.to_lowercase().contains("error rate") {
462            path = format!("{}/error_rate", path);
463        } else if suggestion_title.to_lowercase().contains("schema") {
464            path = format!("{}/schema", path);
465        } else if suggestion_title.to_lowercase().contains("example") {
466            path = format!("{}/examples", path);
467        }
468
469        path
470    }
471
472    /// Infer patch value from suggestion and context
473    fn infer_patch_value(
474        &self,
475        suggestion: &DebugSuggestion,
476        context: &FailureContext,
477    ) -> Option<serde_json::Value> {
478        // Generate appropriate value based on suggestion type
479        if suggestion.title.contains("422") || suggestion.description.contains("422") {
480            // Add 422 validation error example
481            Some(serde_json::json!({
482                "status": 422,
483                "body": {
484                    "error": "Validation failed",
485                    "message": context.error_message.clone().unwrap_or_else(|| "Invalid request".to_string())
486                }
487            }))
488        } else if suggestion.title.contains("schema") || suggestion.description.contains("schema") {
489            // Schema tightening - suggest number type for amount fields
490            if suggestion.description.contains("amount") {
491                Some(serde_json::json!({
492                    "type": "number",
493                    "format": "float"
494                }))
495            } else {
496                Some(serde_json::json!({
497                    "type": "string"
498                }))
499            }
500        } else if suggestion.title.contains("persona") || suggestion.description.contains("persona")
501        {
502            // Persona configuration
503            Some(serde_json::json!({
504                "traits": {},
505                "domain": "general"
506            }))
507        } else {
508            // Generic configuration value
509            Some(serde_json::json!({
510                "enabled": true
511            }))
512        }
513    }
514
515    /// Identify related mock configurations
516    fn identify_related_configs(
517        &self,
518        context: &FailureContext,
519        unified_context: Option<&UnifiedDebugContext>,
520    ) -> Vec<String> {
521        let mut configs = Vec::new();
522
523        // Add contract validation config if present
524        if context.contract_validation.is_some() {
525            configs.push("Contract Validation".to_string());
526        }
527
528        // Add persona configs if behavioral rules are present
529        if !context.behavioral_rules.is_empty() {
530            configs.push("Persona Configuration".to_string());
531        }
532
533        // Add chaos configs if present
534        if !context.chaos_configs.is_empty() {
535            configs.push("Chaos Configuration".to_string());
536        }
537
538        // Add consistency rules if present
539        if !context.consistency_rules.is_empty() {
540            configs.push("Consistency Rules".to_string());
541        }
542
543        // Enhance with unified context information
544        if let Some(uc) = unified_context {
545            if uc.reality.level.is_some() {
546                configs.push(format!(
547                    "Reality Level: {}",
548                    uc.reality.level_name.as_ref().unwrap_or(&"Unknown".to_string())
549                ));
550            }
551            if uc.scenario.active_scenario.is_some() {
552                configs.push(format!(
553                    "Active Scenario: {}",
554                    uc.scenario.active_scenario.as_ref().unwrap()
555                ));
556            }
557            if uc.persona.active_persona_id.is_some() {
558                configs.push(format!(
559                    "Active Persona: {}",
560                    uc.persona.active_persona_id.as_ref().unwrap()
561                ));
562            }
563            if !uc.contract.active_contracts.is_empty() {
564                configs
565                    .push(format!("Active Contracts: {}", uc.contract.active_contracts.join(", ")));
566            }
567        }
568
569        // Add reality continuum if no specific configs found
570        if configs.is_empty() {
571            configs.push("Reality Continuum Settings".to_string());
572        }
573
574        configs
575    }
576}
577
578impl Default for DebugAnalyzer {
579    fn default() -> Self {
580        Self::new()
581    }
582}
583
584/// Parsed failure information from test logs
585#[derive(Debug, Default)]
586struct ParsedFailureInfo {
587    method: Option<String>,
588    path: Option<String>,
589    status_code: Option<u16>,
590    error_message: Option<String>,
591}
592
593/// Request for debug analysis
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct DebugRequest {
596    /// Test failure logs
597    pub test_logs: String,
598
599    /// Test name/identifier
600    pub test_name: Option<String>,
601
602    /// Workspace ID for context
603    pub workspace_id: Option<String>,
604}
605
606/// Response from debug analysis
607#[derive(Debug, Clone, Serialize, Deserialize)]
608pub struct DebugResponse {
609    /// Identified root cause
610    pub root_cause: String,
611
612    /// Suggested fixes
613    pub suggestions: Vec<DebugSuggestion>,
614
615    /// Related mock configurations
616    pub related_configs: Vec<String>,
617
618    /// Full failure context (optional, for detailed analysis)
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub context: Option<FailureContext>,
621
622    /// Unified debug context from subsystems (optional)
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub unified_context: Option<UnifiedDebugContext>,
625}
626
627/// Debug suggestion for fixing a test failure
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct DebugSuggestion {
630    /// Suggestion title
631    pub title: String,
632
633    /// Detailed description
634    pub description: String,
635
636    /// Suggested action
637    pub action: String,
638
639    /// Configuration path to update
640    pub config_path: Option<String>,
641
642    /// JSON Patch operation for applying the fix (optional)
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub patch: Option<DebugPatch>,
645
646    /// Linked artifacts (persona IDs, scenario names, contract paths)
647    #[serde(default, skip_serializing_if = "Vec::is_empty")]
648    pub linked_artifacts: Vec<LinkedArtifact>,
649}
650
651/// Linked artifact reference
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct LinkedArtifact {
654    /// Artifact type (persona, scenario, contract, reality)
655    pub artifact_type: String,
656    /// Artifact ID or path
657    pub artifact_id: String,
658    /// Artifact name (optional)
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub artifact_name: Option<String>,
661}
662
663/// JSON Patch operation for applying a debug suggestion
664#[derive(Debug, Clone, Serialize, Deserialize)]
665pub struct DebugPatch {
666    /// Patch operation type: "add", "remove", or "replace"
667    pub op: String,
668
669    /// JSON Pointer path to the field to modify
670    pub path: String,
671
672    /// Value to add or replace (for "add" and "replace" operations)
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub value: Option<serde_json::Value>,
675
676    /// Source path for "move" or "copy" operations
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub from: Option<String>,
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use crate::ai_studio::debug_context_integrator::DebugContextIntegrator;
685    use crate::intelligent_behavior::config::BehaviorModelConfig;
686    use serde_json::json;
687
688    fn create_test_config() -> IntelligentBehaviorConfig {
689        IntelligentBehaviorConfig {
690            behavior_model: BehaviorModelConfig {
691                llm_provider: "ollama".to_string(),
692                model: "llama2".to_string(),
693                api_endpoint: Some("http://localhost:11434/api/chat".to_string()),
694                api_key: None,
695                temperature: 0.7,
696                max_tokens: 2000,
697                rules: crate::intelligent_behavior::types::BehaviorRules::default(),
698            },
699            ..Default::default()
700        }
701    }
702
703    #[test]
704    fn test_debug_analyzer_new() {
705        let analyzer = DebugAnalyzer::new();
706        // Just verify it can be created
707        let _ = analyzer;
708    }
709
710    #[test]
711    fn test_debug_analyzer_default() {
712        let analyzer = DebugAnalyzer::default();
713        // Just verify it can be created
714        let _ = analyzer;
715    }
716
717    #[test]
718    fn test_debug_analyzer_with_config() {
719        let config = create_test_config();
720        let analyzer = DebugAnalyzer::with_config(config);
721        // Just verify it can be created
722        let _ = analyzer;
723    }
724
725    #[test]
726    fn test_debug_analyzer_with_integrator() {
727        // Create a minimal integrator for testing
728        // Note: This might fail if DebugContextIntegrator::new() requires parameters
729        // In that case, we'll need to adjust the test
730        let integrator = DebugContextIntegrator::new();
731        let analyzer = DebugAnalyzer::with_integrator(integrator);
732        // Just verify it can be created
733        let _ = analyzer;
734    }
735
736    #[test]
737    fn test_debug_analyzer_with_config_and_integrator() {
738        let config = create_test_config();
739        let integrator = DebugContextIntegrator::new();
740        let analyzer = DebugAnalyzer::with_config_and_integrator(config, integrator);
741        // Just verify it can be created
742        let _ = analyzer;
743    }
744
745    #[test]
746    fn test_debug_request_creation() {
747        let request = DebugRequest {
748            test_logs: "GET /api/users 404".to_string(),
749            test_name: Some("test_get_user".to_string()),
750            workspace_id: Some("ws-123".to_string()),
751        };
752
753        assert_eq!(request.test_logs, "GET /api/users 404");
754        assert_eq!(request.test_name, Some("test_get_user".to_string()));
755        assert_eq!(request.workspace_id, Some("ws-123".to_string()));
756    }
757
758    #[test]
759    fn test_debug_request_serialization() {
760        let request = DebugRequest {
761            test_logs: "Error: 500 Internal Server Error".to_string(),
762            test_name: None,
763            workspace_id: None,
764        };
765
766        let json = serde_json::to_string(&request).unwrap();
767        assert!(json.contains("Error: 500"));
768    }
769
770    #[test]
771    fn test_debug_response_creation() {
772        let response = DebugResponse {
773            root_cause: "Authentication failed".to_string(),
774            suggestions: vec![],
775            related_configs: vec!["Persona: admin".to_string()],
776            context: None,
777            unified_context: None,
778        };
779
780        assert_eq!(response.root_cause, "Authentication failed");
781        assert_eq!(response.related_configs.len(), 1);
782    }
783
784    #[test]
785    fn test_debug_response_serialization() {
786        let response = DebugResponse {
787            root_cause: "Root cause".to_string(),
788            suggestions: vec![],
789            related_configs: vec![],
790            context: None,
791            unified_context: None,
792        };
793
794        let json = serde_json::to_string(&response).unwrap();
795        assert!(json.contains("Root cause"));
796    }
797
798    #[test]
799    fn test_debug_suggestion_creation() {
800        let suggestion = DebugSuggestion {
801            title: "Fix authentication".to_string(),
802            description: "Update the auth token".to_string(),
803            action: "Update config".to_string(),
804            config_path: Some("/auth/token".to_string()),
805            patch: None,
806            linked_artifacts: vec![],
807        };
808
809        assert_eq!(suggestion.title, "Fix authentication");
810        assert_eq!(suggestion.config_path, Some("/auth/token".to_string()));
811    }
812
813    #[test]
814    fn test_debug_suggestion_serialization() {
815        let suggestion = DebugSuggestion {
816            title: "Test suggestion".to_string(),
817            description: "Test description".to_string(),
818            action: "Test action".to_string(),
819            config_path: None,
820            patch: None,
821            linked_artifacts: vec![],
822        };
823
824        let json = serde_json::to_string(&suggestion).unwrap();
825        assert!(json.contains("Test suggestion"));
826    }
827
828    #[test]
829    fn test_linked_artifact_creation() {
830        let artifact = LinkedArtifact {
831            artifact_type: "persona".to_string(),
832            artifact_id: "persona-123".to_string(),
833            artifact_name: Some("Admin Persona".to_string()),
834        };
835
836        assert_eq!(artifact.artifact_type, "persona");
837        assert_eq!(artifact.artifact_id, "persona-123");
838        assert_eq!(artifact.artifact_name, Some("Admin Persona".to_string()));
839    }
840
841    #[test]
842    fn test_linked_artifact_serialization() {
843        let artifact = LinkedArtifact {
844            artifact_type: "scenario".to_string(),
845            artifact_id: "scenario-456".to_string(),
846            artifact_name: None,
847        };
848
849        let json = serde_json::to_string(&artifact).unwrap();
850        assert!(json.contains("scenario"));
851        assert!(json.contains("scenario-456"));
852    }
853
854    #[test]
855    fn test_debug_patch_creation() {
856        let patch = DebugPatch {
857            op: "replace".to_string(),
858            path: "/status".to_string(),
859            value: Some(json!("active")),
860            from: None,
861        };
862
863        assert_eq!(patch.op, "replace");
864        assert_eq!(patch.path, "/status");
865        assert!(patch.value.is_some());
866    }
867
868    #[test]
869    fn test_debug_patch_serialization() {
870        let patch = DebugPatch {
871            op: "add".to_string(),
872            path: "/new_field".to_string(),
873            value: Some(json!({"key": "value"})),
874            from: None,
875        };
876
877        let json = serde_json::to_string(&patch).unwrap();
878        assert!(json.contains("add"));
879        assert!(json.contains("new_field"));
880    }
881
882    #[test]
883    fn test_debug_patch_with_from() {
884        let patch = DebugPatch {
885            op: "move".to_string(),
886            path: "/target".to_string(),
887            value: None,
888            from: Some("/source".to_string()),
889        };
890
891        assert_eq!(patch.op, "move");
892        assert_eq!(patch.from, Some("/source".to_string()));
893    }
894
895    #[test]
896    fn test_parsed_failure_info_default() {
897        let info = ParsedFailureInfo::default();
898        assert!(info.method.is_none());
899        assert!(info.path.is_none());
900        assert!(info.status_code.is_none());
901        assert!(info.error_message.is_none());
902    }
903
904    #[test]
905    fn test_debug_request_clone() {
906        let request1 = DebugRequest {
907            test_logs: "GET /api/test 404".to_string(),
908            test_name: Some("test".to_string()),
909            workspace_id: Some("ws-1".to_string()),
910        };
911        let request2 = request1.clone();
912        assert_eq!(request1.test_logs, request2.test_logs);
913    }
914
915    #[test]
916    fn test_debug_request_debug() {
917        let request = DebugRequest {
918            test_logs: "Error occurred".to_string(),
919            test_name: None,
920            workspace_id: None,
921        };
922        let debug_str = format!("{:?}", request);
923        assert!(debug_str.contains("DebugRequest"));
924    }
925
926    #[test]
927    fn test_debug_response_clone() {
928        let response1 = DebugResponse {
929            root_cause: "Root cause".to_string(),
930            suggestions: vec![],
931            related_configs: vec![],
932            context: None,
933            unified_context: None,
934        };
935        let response2 = response1.clone();
936        assert_eq!(response1.root_cause, response2.root_cause);
937    }
938
939    #[test]
940    fn test_debug_response_debug() {
941        let response = DebugResponse {
942            root_cause: "Test root cause".to_string(),
943            suggestions: vec![],
944            related_configs: vec!["config1".to_string()],
945            context: None,
946            unified_context: None,
947        };
948        let debug_str = format!("{:?}", response);
949        assert!(debug_str.contains("DebugResponse"));
950    }
951
952    #[test]
953    fn test_debug_suggestion_clone() {
954        let suggestion1 = DebugSuggestion {
955            title: "Fix issue".to_string(),
956            description: "Description".to_string(),
957            action: "Action".to_string(),
958            config_path: None,
959            patch: None,
960            linked_artifacts: vec![],
961        };
962        let suggestion2 = suggestion1.clone();
963        assert_eq!(suggestion1.title, suggestion2.title);
964    }
965
966    #[test]
967    fn test_debug_suggestion_debug() {
968        let suggestion = DebugSuggestion {
969            title: "Test suggestion".to_string(),
970            description: "Test description".to_string(),
971            action: "Test action".to_string(),
972            config_path: Some("/config/path".to_string()),
973            patch: None,
974            linked_artifacts: vec![],
975        };
976        let debug_str = format!("{:?}", suggestion);
977        assert!(debug_str.contains("DebugSuggestion"));
978    }
979
980    #[test]
981    fn test_linked_artifact_clone() {
982        let artifact1 = LinkedArtifact {
983            artifact_type: "persona".to_string(),
984            artifact_id: "id-1".to_string(),
985            artifact_name: Some("Name".to_string()),
986        };
987        let artifact2 = artifact1.clone();
988        assert_eq!(artifact1.artifact_type, artifact2.artifact_type);
989    }
990
991    #[test]
992    fn test_linked_artifact_debug() {
993        let artifact = LinkedArtifact {
994            artifact_type: "scenario".to_string(),
995            artifact_id: "id-2".to_string(),
996            artifact_name: None,
997        };
998        let debug_str = format!("{:?}", artifact);
999        assert!(debug_str.contains("LinkedArtifact"));
1000    }
1001
1002    #[test]
1003    fn test_debug_patch_clone() {
1004        let patch1 = DebugPatch {
1005            op: "replace".to_string(),
1006            path: "/path".to_string(),
1007            value: Some(json!("value")),
1008            from: None,
1009        };
1010        let patch2 = patch1.clone();
1011        assert_eq!(patch1.op, patch2.op);
1012    }
1013
1014    #[test]
1015    fn test_debug_patch_debug() {
1016        let patch = DebugPatch {
1017            op: "add".to_string(),
1018            path: "/new".to_string(),
1019            value: None,
1020            from: Some("/old".to_string()),
1021        };
1022        let debug_str = format!("{:?}", patch);
1023        assert!(debug_str.contains("DebugPatch"));
1024    }
1025
1026    #[test]
1027    fn test_parsed_failure_info_creation() {
1028        let info = ParsedFailureInfo {
1029            method: Some("POST".to_string()),
1030            path: Some("/api/users".to_string()),
1031            status_code: Some(500),
1032            error_message: Some("Internal error".to_string()),
1033        };
1034        assert_eq!(info.method, Some("POST".to_string()));
1035        assert_eq!(info.status_code, Some(500));
1036    }
1037
1038    #[test]
1039    fn test_debug_request_with_all_fields() {
1040        let request = DebugRequest {
1041            test_logs: "POST /api/users 201\nResponse: {\"id\": 1}".to_string(),
1042            test_name: Some("test_create_user".to_string()),
1043            workspace_id: Some("workspace-123".to_string()),
1044        };
1045        assert!(!request.test_logs.is_empty());
1046        assert!(request.test_name.is_some());
1047        assert!(request.workspace_id.is_some());
1048    }
1049
1050    #[test]
1051    fn test_debug_response_with_all_fields() {
1052        let suggestion = DebugSuggestion {
1053            title: "Fix auth".to_string(),
1054            description: "Update token".to_string(),
1055            action: "Update config".to_string(),
1056            config_path: Some("/auth/token".to_string()),
1057            patch: Some(DebugPatch {
1058                op: "replace".to_string(),
1059                path: "/auth/token".to_string(),
1060                value: Some(json!("new-token")),
1061                from: None,
1062            }),
1063            linked_artifacts: vec![LinkedArtifact {
1064                artifact_type: "persona".to_string(),
1065                artifact_id: "persona-1".to_string(),
1066                artifact_name: Some("Admin".to_string()),
1067            }],
1068        };
1069        let response = DebugResponse {
1070            root_cause: "Authentication failed".to_string(),
1071            suggestions: vec![suggestion],
1072            related_configs: vec!["Persona: admin".to_string(), "Scenario: auth".to_string()],
1073            context: None,
1074            unified_context: None,
1075        };
1076        assert_eq!(response.suggestions.len(), 1);
1077        assert_eq!(response.related_configs.len(), 2);
1078    }
1079
1080    #[test]
1081    fn test_debug_suggestion_with_patch() {
1082        let patch = DebugPatch {
1083            op: "replace".to_string(),
1084            path: "/status".to_string(),
1085            value: Some(json!("active")),
1086            from: None,
1087        };
1088        let suggestion = DebugSuggestion {
1089            title: "Update status".to_string(),
1090            description: "Change status to active".to_string(),
1091            action: "Apply patch".to_string(),
1092            config_path: Some("/status".to_string()),
1093            patch: Some(patch.clone()),
1094            linked_artifacts: vec![],
1095        };
1096        assert!(suggestion.patch.is_some());
1097        assert_eq!(suggestion.patch.unwrap().op, "replace");
1098    }
1099
1100    #[test]
1101    fn test_debug_patch_all_operations() {
1102        let operations = vec!["add", "remove", "replace", "move", "copy"];
1103        for op in operations {
1104            let patch = DebugPatch {
1105                op: op.to_string(),
1106                path: "/test".to_string(),
1107                value: Some(json!("value")),
1108                from: None,
1109            };
1110            assert_eq!(patch.op, op);
1111        }
1112    }
1113
1114    #[test]
1115    fn test_linked_artifact_with_name() {
1116        let artifact = LinkedArtifact {
1117            artifact_type: "persona".to_string(),
1118            artifact_id: "persona-123".to_string(),
1119            artifact_name: Some("Admin Persona".to_string()),
1120        };
1121        assert_eq!(artifact.artifact_type, "persona");
1122        assert!(artifact.artifact_name.is_some());
1123    }
1124
1125    #[test]
1126    fn test_linked_artifact_without_name() {
1127        let artifact = LinkedArtifact {
1128            artifact_type: "scenario".to_string(),
1129            artifact_id: "scenario-456".to_string(),
1130            artifact_name: None,
1131        };
1132        assert_eq!(artifact.artifact_type, "scenario");
1133        assert!(artifact.artifact_name.is_none());
1134    }
1135
1136    #[test]
1137    fn test_parsed_failure_info_with_all_fields() {
1138        let info = ParsedFailureInfo {
1139            method: Some("PUT".to_string()),
1140            path: Some("/api/users/123".to_string()),
1141            status_code: Some(422),
1142            error_message: Some("Validation failed: email is required".to_string()),
1143        };
1144        assert_eq!(info.method, Some("PUT".to_string()));
1145        assert_eq!(info.path, Some("/api/users/123".to_string()));
1146        assert_eq!(info.status_code, Some(422));
1147        assert!(info.error_message.is_some());
1148    }
1149}