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 let Some(active_scenario) = uc.scenario.active_scenario.as_ref() {
552                configs.push(format!("Active Scenario: {}", active_scenario));
553            }
554            if let Some(active_persona_id) = uc.persona.active_persona_id.as_ref() {
555                configs.push(format!("Active Persona: {}", active_persona_id));
556            }
557            if !uc.contract.active_contracts.is_empty() {
558                configs
559                    .push(format!("Active Contracts: {}", uc.contract.active_contracts.join(", ")));
560            }
561        }
562
563        // Add reality continuum if no specific configs found
564        if configs.is_empty() {
565            configs.push("Reality Continuum Settings".to_string());
566        }
567
568        configs
569    }
570}
571
572impl Default for DebugAnalyzer {
573    fn default() -> Self {
574        Self::new()
575    }
576}
577
578/// Parsed failure information from test logs
579#[derive(Debug, Default)]
580struct ParsedFailureInfo {
581    method: Option<String>,
582    path: Option<String>,
583    status_code: Option<u16>,
584    error_message: Option<String>,
585}
586
587/// Request for debug analysis
588#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct DebugRequest {
590    /// Test failure logs
591    pub test_logs: String,
592
593    /// Test name/identifier
594    pub test_name: Option<String>,
595
596    /// Workspace ID for context
597    pub workspace_id: Option<String>,
598}
599
600/// Response from debug analysis
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct DebugResponse {
603    /// Identified root cause
604    pub root_cause: String,
605
606    /// Suggested fixes
607    pub suggestions: Vec<DebugSuggestion>,
608
609    /// Related mock configurations
610    pub related_configs: Vec<String>,
611
612    /// Full failure context (optional, for detailed analysis)
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub context: Option<FailureContext>,
615
616    /// Unified debug context from subsystems (optional)
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub unified_context: Option<UnifiedDebugContext>,
619}
620
621/// Debug suggestion for fixing a test failure
622#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct DebugSuggestion {
624    /// Suggestion title
625    pub title: String,
626
627    /// Detailed description
628    pub description: String,
629
630    /// Suggested action
631    pub action: String,
632
633    /// Configuration path to update
634    pub config_path: Option<String>,
635
636    /// JSON Patch operation for applying the fix (optional)
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub patch: Option<DebugPatch>,
639
640    /// Linked artifacts (persona IDs, scenario names, contract paths)
641    #[serde(default, skip_serializing_if = "Vec::is_empty")]
642    pub linked_artifacts: Vec<LinkedArtifact>,
643}
644
645/// Linked artifact reference
646#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct LinkedArtifact {
648    /// Artifact type (persona, scenario, contract, reality)
649    pub artifact_type: String,
650    /// Artifact ID or path
651    pub artifact_id: String,
652    /// Artifact name (optional)
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub artifact_name: Option<String>,
655}
656
657/// JSON Patch operation for applying a debug suggestion
658#[derive(Debug, Clone, Serialize, Deserialize)]
659pub struct DebugPatch {
660    /// Patch operation type: "add", "remove", or "replace"
661    pub op: String,
662
663    /// JSON Pointer path to the field to modify
664    pub path: String,
665
666    /// Value to add or replace (for "add" and "replace" operations)
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub value: Option<serde_json::Value>,
669
670    /// Source path for "move" or "copy" operations
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub from: Option<String>,
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    use crate::ai_studio::debug_context_integrator::DebugContextIntegrator;
679    use crate::intelligent_behavior::config::BehaviorModelConfig;
680    use serde_json::json;
681
682    fn create_test_config() -> IntelligentBehaviorConfig {
683        IntelligentBehaviorConfig {
684            behavior_model: BehaviorModelConfig {
685                llm_provider: "ollama".to_string(),
686                model: "llama2".to_string(),
687                api_endpoint: Some("http://localhost:11434/api/chat".to_string()),
688                api_key: None,
689                temperature: 0.7,
690                max_tokens: 2000,
691                rules: crate::intelligent_behavior::types::BehaviorRules::default(),
692            },
693            ..Default::default()
694        }
695    }
696
697    #[test]
698    fn test_debug_analyzer_new() {
699        let analyzer = DebugAnalyzer::new();
700        // Just verify it can be created
701        let _ = analyzer;
702    }
703
704    #[test]
705    fn test_debug_analyzer_default() {
706        let analyzer = DebugAnalyzer::default();
707        // Just verify it can be created
708        let _ = analyzer;
709    }
710
711    #[test]
712    fn test_debug_analyzer_with_config() {
713        let config = create_test_config();
714        let analyzer = DebugAnalyzer::with_config(config);
715        // Just verify it can be created
716        let _ = analyzer;
717    }
718
719    #[test]
720    fn test_debug_analyzer_with_integrator() {
721        // Create a minimal integrator for testing
722        // Note: This might fail if DebugContextIntegrator::new() requires parameters
723        // In that case, we'll need to adjust the test
724        let integrator = DebugContextIntegrator::new();
725        let analyzer = DebugAnalyzer::with_integrator(integrator);
726        // Just verify it can be created
727        let _ = analyzer;
728    }
729
730    #[test]
731    fn test_debug_analyzer_with_config_and_integrator() {
732        let config = create_test_config();
733        let integrator = DebugContextIntegrator::new();
734        let analyzer = DebugAnalyzer::with_config_and_integrator(config, integrator);
735        // Just verify it can be created
736        let _ = analyzer;
737    }
738
739    #[test]
740    fn test_debug_request_creation() {
741        let request = DebugRequest {
742            test_logs: "GET /api/users 404".to_string(),
743            test_name: Some("test_get_user".to_string()),
744            workspace_id: Some("ws-123".to_string()),
745        };
746
747        assert_eq!(request.test_logs, "GET /api/users 404");
748        assert_eq!(request.test_name, Some("test_get_user".to_string()));
749        assert_eq!(request.workspace_id, Some("ws-123".to_string()));
750    }
751
752    #[test]
753    fn test_debug_request_serialization() {
754        let request = DebugRequest {
755            test_logs: "Error: 500 Internal Server Error".to_string(),
756            test_name: None,
757            workspace_id: None,
758        };
759
760        let json = serde_json::to_string(&request).unwrap();
761        assert!(json.contains("Error: 500"));
762    }
763
764    #[test]
765    fn test_debug_response_creation() {
766        let response = DebugResponse {
767            root_cause: "Authentication failed".to_string(),
768            suggestions: vec![],
769            related_configs: vec!["Persona: admin".to_string()],
770            context: None,
771            unified_context: None,
772        };
773
774        assert_eq!(response.root_cause, "Authentication failed");
775        assert_eq!(response.related_configs.len(), 1);
776    }
777
778    #[test]
779    fn test_debug_response_serialization() {
780        let response = DebugResponse {
781            root_cause: "Root cause".to_string(),
782            suggestions: vec![],
783            related_configs: vec![],
784            context: None,
785            unified_context: None,
786        };
787
788        let json = serde_json::to_string(&response).unwrap();
789        assert!(json.contains("Root cause"));
790    }
791
792    #[test]
793    fn test_debug_suggestion_creation() {
794        let suggestion = DebugSuggestion {
795            title: "Fix authentication".to_string(),
796            description: "Update the auth token".to_string(),
797            action: "Update config".to_string(),
798            config_path: Some("/auth/token".to_string()),
799            patch: None,
800            linked_artifacts: vec![],
801        };
802
803        assert_eq!(suggestion.title, "Fix authentication");
804        assert_eq!(suggestion.config_path, Some("/auth/token".to_string()));
805    }
806
807    #[test]
808    fn test_debug_suggestion_serialization() {
809        let suggestion = DebugSuggestion {
810            title: "Test suggestion".to_string(),
811            description: "Test description".to_string(),
812            action: "Test action".to_string(),
813            config_path: None,
814            patch: None,
815            linked_artifacts: vec![],
816        };
817
818        let json = serde_json::to_string(&suggestion).unwrap();
819        assert!(json.contains("Test suggestion"));
820    }
821
822    #[test]
823    fn test_linked_artifact_creation() {
824        let artifact = LinkedArtifact {
825            artifact_type: "persona".to_string(),
826            artifact_id: "persona-123".to_string(),
827            artifact_name: Some("Admin Persona".to_string()),
828        };
829
830        assert_eq!(artifact.artifact_type, "persona");
831        assert_eq!(artifact.artifact_id, "persona-123");
832        assert_eq!(artifact.artifact_name, Some("Admin Persona".to_string()));
833    }
834
835    #[test]
836    fn test_linked_artifact_serialization() {
837        let artifact = LinkedArtifact {
838            artifact_type: "scenario".to_string(),
839            artifact_id: "scenario-456".to_string(),
840            artifact_name: None,
841        };
842
843        let json = serde_json::to_string(&artifact).unwrap();
844        assert!(json.contains("scenario"));
845        assert!(json.contains("scenario-456"));
846    }
847
848    #[test]
849    fn test_debug_patch_creation() {
850        let patch = DebugPatch {
851            op: "replace".to_string(),
852            path: "/status".to_string(),
853            value: Some(json!("active")),
854            from: None,
855        };
856
857        assert_eq!(patch.op, "replace");
858        assert_eq!(patch.path, "/status");
859        assert!(patch.value.is_some());
860    }
861
862    #[test]
863    fn test_debug_patch_serialization() {
864        let patch = DebugPatch {
865            op: "add".to_string(),
866            path: "/new_field".to_string(),
867            value: Some(json!({"key": "value"})),
868            from: None,
869        };
870
871        let json = serde_json::to_string(&patch).unwrap();
872        assert!(json.contains("add"));
873        assert!(json.contains("new_field"));
874    }
875
876    #[test]
877    fn test_debug_patch_with_from() {
878        let patch = DebugPatch {
879            op: "move".to_string(),
880            path: "/target".to_string(),
881            value: None,
882            from: Some("/source".to_string()),
883        };
884
885        assert_eq!(patch.op, "move");
886        assert_eq!(patch.from, Some("/source".to_string()));
887    }
888
889    #[test]
890    fn test_parsed_failure_info_default() {
891        let info = ParsedFailureInfo::default();
892        assert!(info.method.is_none());
893        assert!(info.path.is_none());
894        assert!(info.status_code.is_none());
895        assert!(info.error_message.is_none());
896    }
897
898    #[test]
899    fn test_debug_request_clone() {
900        let request1 = DebugRequest {
901            test_logs: "GET /api/test 404".to_string(),
902            test_name: Some("test".to_string()),
903            workspace_id: Some("ws-1".to_string()),
904        };
905        let request2 = request1.clone();
906        assert_eq!(request1.test_logs, request2.test_logs);
907    }
908
909    #[test]
910    fn test_debug_request_debug() {
911        let request = DebugRequest {
912            test_logs: "Error occurred".to_string(),
913            test_name: None,
914            workspace_id: None,
915        };
916        let debug_str = format!("{:?}", request);
917        assert!(debug_str.contains("DebugRequest"));
918    }
919
920    #[test]
921    fn test_debug_response_clone() {
922        let response1 = DebugResponse {
923            root_cause: "Root cause".to_string(),
924            suggestions: vec![],
925            related_configs: vec![],
926            context: None,
927            unified_context: None,
928        };
929        let response2 = response1.clone();
930        assert_eq!(response1.root_cause, response2.root_cause);
931    }
932
933    #[test]
934    fn test_debug_response_debug() {
935        let response = DebugResponse {
936            root_cause: "Test root cause".to_string(),
937            suggestions: vec![],
938            related_configs: vec!["config1".to_string()],
939            context: None,
940            unified_context: None,
941        };
942        let debug_str = format!("{:?}", response);
943        assert!(debug_str.contains("DebugResponse"));
944    }
945
946    #[test]
947    fn test_debug_suggestion_clone() {
948        let suggestion1 = DebugSuggestion {
949            title: "Fix issue".to_string(),
950            description: "Description".to_string(),
951            action: "Action".to_string(),
952            config_path: None,
953            patch: None,
954            linked_artifacts: vec![],
955        };
956        let suggestion2 = suggestion1.clone();
957        assert_eq!(suggestion1.title, suggestion2.title);
958    }
959
960    #[test]
961    fn test_debug_suggestion_debug() {
962        let suggestion = DebugSuggestion {
963            title: "Test suggestion".to_string(),
964            description: "Test description".to_string(),
965            action: "Test action".to_string(),
966            config_path: Some("/config/path".to_string()),
967            patch: None,
968            linked_artifacts: vec![],
969        };
970        let debug_str = format!("{:?}", suggestion);
971        assert!(debug_str.contains("DebugSuggestion"));
972    }
973
974    #[test]
975    fn test_linked_artifact_clone() {
976        let artifact1 = LinkedArtifact {
977            artifact_type: "persona".to_string(),
978            artifact_id: "id-1".to_string(),
979            artifact_name: Some("Name".to_string()),
980        };
981        let artifact2 = artifact1.clone();
982        assert_eq!(artifact1.artifact_type, artifact2.artifact_type);
983    }
984
985    #[test]
986    fn test_linked_artifact_debug() {
987        let artifact = LinkedArtifact {
988            artifact_type: "scenario".to_string(),
989            artifact_id: "id-2".to_string(),
990            artifact_name: None,
991        };
992        let debug_str = format!("{:?}", artifact);
993        assert!(debug_str.contains("LinkedArtifact"));
994    }
995
996    #[test]
997    fn test_debug_patch_clone() {
998        let patch1 = DebugPatch {
999            op: "replace".to_string(),
1000            path: "/path".to_string(),
1001            value: Some(json!("value")),
1002            from: None,
1003        };
1004        let patch2 = patch1.clone();
1005        assert_eq!(patch1.op, patch2.op);
1006    }
1007
1008    #[test]
1009    fn test_debug_patch_debug() {
1010        let patch = DebugPatch {
1011            op: "add".to_string(),
1012            path: "/new".to_string(),
1013            value: None,
1014            from: Some("/old".to_string()),
1015        };
1016        let debug_str = format!("{:?}", patch);
1017        assert!(debug_str.contains("DebugPatch"));
1018    }
1019
1020    #[test]
1021    fn test_parsed_failure_info_creation() {
1022        let info = ParsedFailureInfo {
1023            method: Some("POST".to_string()),
1024            path: Some("/api/users".to_string()),
1025            status_code: Some(500),
1026            error_message: Some("Internal error".to_string()),
1027        };
1028        assert_eq!(info.method, Some("POST".to_string()));
1029        assert_eq!(info.status_code, Some(500));
1030    }
1031
1032    #[test]
1033    fn test_debug_request_with_all_fields() {
1034        let request = DebugRequest {
1035            test_logs: "POST /api/users 201\nResponse: {\"id\": 1}".to_string(),
1036            test_name: Some("test_create_user".to_string()),
1037            workspace_id: Some("workspace-123".to_string()),
1038        };
1039        assert!(!request.test_logs.is_empty());
1040        assert!(request.test_name.is_some());
1041        assert!(request.workspace_id.is_some());
1042    }
1043
1044    #[test]
1045    fn test_debug_response_with_all_fields() {
1046        let suggestion = DebugSuggestion {
1047            title: "Fix auth".to_string(),
1048            description: "Update token".to_string(),
1049            action: "Update config".to_string(),
1050            config_path: Some("/auth/token".to_string()),
1051            patch: Some(DebugPatch {
1052                op: "replace".to_string(),
1053                path: "/auth/token".to_string(),
1054                value: Some(json!("new-token")),
1055                from: None,
1056            }),
1057            linked_artifacts: vec![LinkedArtifact {
1058                artifact_type: "persona".to_string(),
1059                artifact_id: "persona-1".to_string(),
1060                artifact_name: Some("Admin".to_string()),
1061            }],
1062        };
1063        let response = DebugResponse {
1064            root_cause: "Authentication failed".to_string(),
1065            suggestions: vec![suggestion],
1066            related_configs: vec!["Persona: admin".to_string(), "Scenario: auth".to_string()],
1067            context: None,
1068            unified_context: None,
1069        };
1070        assert_eq!(response.suggestions.len(), 1);
1071        assert_eq!(response.related_configs.len(), 2);
1072    }
1073
1074    #[test]
1075    fn test_debug_suggestion_with_patch() {
1076        let patch = DebugPatch {
1077            op: "replace".to_string(),
1078            path: "/status".to_string(),
1079            value: Some(json!("active")),
1080            from: None,
1081        };
1082        let suggestion = DebugSuggestion {
1083            title: "Update status".to_string(),
1084            description: "Change status to active".to_string(),
1085            action: "Apply patch".to_string(),
1086            config_path: Some("/status".to_string()),
1087            patch: Some(patch.clone()),
1088            linked_artifacts: vec![],
1089        };
1090        assert!(suggestion.patch.is_some());
1091        assert_eq!(suggestion.patch.unwrap().op, "replace");
1092    }
1093
1094    #[test]
1095    fn test_debug_patch_all_operations() {
1096        let operations = vec!["add", "remove", "replace", "move", "copy"];
1097        for op in operations {
1098            let patch = DebugPatch {
1099                op: op.to_string(),
1100                path: "/test".to_string(),
1101                value: Some(json!("value")),
1102                from: None,
1103            };
1104            assert_eq!(patch.op, op);
1105        }
1106    }
1107
1108    #[test]
1109    fn test_linked_artifact_with_name() {
1110        let artifact = LinkedArtifact {
1111            artifact_type: "persona".to_string(),
1112            artifact_id: "persona-123".to_string(),
1113            artifact_name: Some("Admin Persona".to_string()),
1114        };
1115        assert_eq!(artifact.artifact_type, "persona");
1116        assert!(artifact.artifact_name.is_some());
1117    }
1118
1119    #[test]
1120    fn test_linked_artifact_without_name() {
1121        let artifact = LinkedArtifact {
1122            artifact_type: "scenario".to_string(),
1123            artifact_id: "scenario-456".to_string(),
1124            artifact_name: None,
1125        };
1126        assert_eq!(artifact.artifact_type, "scenario");
1127        assert!(artifact.artifact_name.is_none());
1128    }
1129
1130    #[test]
1131    fn test_parsed_failure_info_with_all_fields() {
1132        let info = ParsedFailureInfo {
1133            method: Some("PUT".to_string()),
1134            path: Some("/api/users/123".to_string()),
1135            status_code: Some(422),
1136            error_message: Some("Validation failed: email is required".to_string()),
1137        };
1138        assert_eq!(info.method, Some("PUT".to_string()));
1139        assert_eq!(info.path, Some("/api/users/123".to_string()));
1140        assert_eq!(info.status_code, Some(422));
1141        assert!(info.error_message.is_some());
1142    }
1143}