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            seed: None,
288        };
289
290        // Generate suggestions from LLM
291        let response = self.llm_client.generate(&llm_request).await?;
292
293        // Parse suggestions from response
294        let mut suggestions: Vec<DebugSuggestion> = if let Some(suggestions_array) =
295            response.get("suggestions")
296        {
297            serde_json::from_value(suggestions_array.clone()).unwrap_or_else(|_| {
298                // Fallback: create a generic suggestion
299                vec![DebugSuggestion {
300                    title: "Review Mock Configuration".to_string(),
301                    description: "Check your mock configuration for issues related to this failure"
302                        .to_string(),
303                    action: "Review config.yaml and related mock settings".to_string(),
304                    config_path: Some("config.yaml".to_string()),
305                    patch: None,
306                    linked_artifacts: Vec::new(),
307                }]
308            })
309        } else {
310            // Fallback suggestions
311            vec![
312                DebugSuggestion {
313                    title: "Check Contract Validation".to_string(),
314                    description: "The failure may be due to contract validation issues. Review your OpenAPI spec and request/response schemas.".to_string(),
315                    action: "Review contract validation settings".to_string(),
316                    config_path: Some("contract_validation".to_string()),
317                    patch: None,
318                    linked_artifacts: Vec::new(),
319                },
320                DebugSuggestion {
321                    title: "Review Persona Settings".to_string(),
322                    description: "The failure might be related to persona configuration. Check if the active persona matches your test expectations.".to_string(),
323                    action: "Review persona configuration".to_string(),
324                    config_path: Some("consistency.personas".to_string()),
325                    patch: None,
326                    linked_artifacts: Vec::new(),
327                },
328            ]
329        };
330
331        // Enhance suggestions with linked artifacts from unified context
332        if let Some(uc) = unified_context {
333            for suggestion in &mut suggestions {
334                // Add persona link if relevant
335                if suggestion.title.to_lowercase().contains("persona")
336                    || suggestion.description.to_lowercase().contains("persona")
337                {
338                    if let Some(ref persona_id) = uc.persona.active_persona_id {
339                        suggestion.linked_artifacts.push(LinkedArtifact {
340                            artifact_type: "persona".to_string(),
341                            artifact_id: persona_id.to_string(),
342                            artifact_name: uc.persona.active_persona_name.clone(),
343                        });
344                    }
345                }
346
347                // Add scenario link if relevant
348                if suggestion.title.to_lowercase().contains("scenario")
349                    || suggestion.description.to_lowercase().contains("scenario")
350                {
351                    if let Some(ref scenario_id) = uc.scenario.active_scenario {
352                        suggestion.linked_artifacts.push(LinkedArtifact {
353                            artifact_type: "scenario".to_string(),
354                            artifact_id: scenario_id.to_string(),
355                            artifact_name: None,
356                        });
357                    }
358                }
359
360                // Add contract links if relevant
361                if suggestion.title.to_lowercase().contains("contract")
362                    || suggestion.description.to_lowercase().contains("contract")
363                {
364                    for contract_path in &uc.contract.active_contracts {
365                        suggestion.linked_artifacts.push(LinkedArtifact {
366                            artifact_type: "contract".to_string(),
367                            artifact_id: contract_path.to_string(),
368                            artifact_name: None,
369                        });
370                    }
371                }
372
373                // Add reality level link if relevant
374                if suggestion.title.to_lowercase().contains("reality")
375                    || suggestion.description.to_lowercase().contains("reality")
376                {
377                    if let Some(ref level_name) = uc.reality.level_name {
378                        suggestion.linked_artifacts.push(LinkedArtifact {
379                            artifact_type: "reality".to_string(),
380                            artifact_id: uc
381                                .reality
382                                .level
383                                .map(|l| l.value().to_string())
384                                .unwrap_or_default(),
385                            artifact_name: Some(level_name.clone()),
386                        });
387                    }
388                }
389            }
390        }
391
392        Ok(suggestions)
393    }
394
395    /// Generate JSON Patch operations for suggestions
396    fn generate_patches(
397        &self,
398        suggestions: &mut [DebugSuggestion],
399        context: &FailureContext,
400        _narrative: &crate::failure_analysis::types::FailureNarrative,
401        _unified_context: Option<&UnifiedDebugContext>,
402    ) -> Result<()> {
403        for suggestion in suggestions.iter_mut() {
404            // Generate patch based on suggestion type and context
405            if let Some(config_path) = &suggestion.config_path {
406                // Generate appropriate patch based on the suggestion
407                let patch = self.create_patch_for_suggestion(suggestion, config_path, context)?;
408                suggestion.patch = patch;
409            }
410        }
411        Ok(())
412    }
413
414    /// Create a JSON Patch operation for a specific suggestion
415    fn create_patch_for_suggestion(
416        &self,
417        suggestion: &DebugSuggestion,
418        config_path: &str,
419        context: &FailureContext,
420    ) -> Result<Option<DebugPatch>> {
421        // Determine patch operation based on suggestion content
422        let patch = if suggestion.action.contains("add") || suggestion.action.contains("Add") {
423            // Add operation - typically for adding new examples or configurations
424            Some(DebugPatch {
425                op: "add".to_string(),
426                path: self.build_patch_path(config_path, &suggestion.title),
427                value: self.infer_patch_value(suggestion, context),
428                from: None,
429            })
430        } else if suggestion.action.contains("remove") || suggestion.action.contains("Remove") {
431            // Remove operation
432            Some(DebugPatch {
433                op: "remove".to_string(),
434                path: self.build_patch_path(config_path, &suggestion.title),
435                value: None,
436                from: None,
437            })
438        } else {
439            // Replace operation (default)
440            Some(DebugPatch {
441                op: "replace".to_string(),
442                path: self.build_patch_path(config_path, &suggestion.title),
443                value: self.infer_patch_value(suggestion, context),
444                from: None,
445            })
446        };
447
448        Ok(patch)
449    }
450
451    /// Build JSON Pointer path from config path and suggestion context
452    fn build_patch_path(&self, config_path: &str, suggestion_title: &str) -> String {
453        // Convert config path to JSON Pointer format
454        // Example: "consistency.personas" -> "/consistency/personas"
455        // Example: "contract_validation" -> "/contract_validation"
456        let mut path = config_path.replace('.', "/");
457        if !path.starts_with('/') {
458            path = format!("/{}", path);
459        }
460
461        // If suggestion mentions a specific field, append it
462        if suggestion_title.to_lowercase().contains("error rate") {
463            path = format!("{}/error_rate", path);
464        } else if suggestion_title.to_lowercase().contains("schema") {
465            path = format!("{}/schema", path);
466        } else if suggestion_title.to_lowercase().contains("example") {
467            path = format!("{}/examples", path);
468        }
469
470        path
471    }
472
473    /// Infer patch value from suggestion and context
474    fn infer_patch_value(
475        &self,
476        suggestion: &DebugSuggestion,
477        context: &FailureContext,
478    ) -> Option<serde_json::Value> {
479        // Generate appropriate value based on suggestion type
480        if suggestion.title.contains("422") || suggestion.description.contains("422") {
481            // Add 422 validation error example
482            Some(serde_json::json!({
483                "status": 422,
484                "body": {
485                    "error": "Validation failed",
486                    "message": context.error_message.clone().unwrap_or_else(|| "Invalid request".to_string())
487                }
488            }))
489        } else if suggestion.title.contains("schema") || suggestion.description.contains("schema") {
490            // Schema tightening - suggest number type for amount fields
491            if suggestion.description.contains("amount") {
492                Some(serde_json::json!({
493                    "type": "number",
494                    "format": "float"
495                }))
496            } else {
497                Some(serde_json::json!({
498                    "type": "string"
499                }))
500            }
501        } else if suggestion.title.contains("persona") || suggestion.description.contains("persona")
502        {
503            // Persona configuration
504            Some(serde_json::json!({
505                "traits": {},
506                "domain": "general"
507            }))
508        } else {
509            // Generic configuration value
510            Some(serde_json::json!({
511                "enabled": true
512            }))
513        }
514    }
515
516    /// Identify related mock configurations
517    fn identify_related_configs(
518        &self,
519        context: &FailureContext,
520        unified_context: Option<&UnifiedDebugContext>,
521    ) -> Vec<String> {
522        let mut configs = Vec::new();
523
524        // Add contract validation config if present
525        if context.contract_validation.is_some() {
526            configs.push("Contract Validation".to_string());
527        }
528
529        // Add persona configs if behavioral rules are present
530        if !context.behavioral_rules.is_empty() {
531            configs.push("Persona Configuration".to_string());
532        }
533
534        // Add chaos configs if present
535        if !context.chaos_configs.is_empty() {
536            configs.push("Chaos Configuration".to_string());
537        }
538
539        // Add consistency rules if present
540        if !context.consistency_rules.is_empty() {
541            configs.push("Consistency Rules".to_string());
542        }
543
544        // Enhance with unified context information
545        if let Some(uc) = unified_context {
546            if uc.reality.level.is_some() {
547                configs.push(format!(
548                    "Reality Level: {}",
549                    uc.reality.level_name.as_ref().unwrap_or(&"Unknown".to_string())
550                ));
551            }
552            if let Some(active_scenario) = uc.scenario.active_scenario.as_ref() {
553                configs.push(format!("Active Scenario: {}", active_scenario));
554            }
555            if let Some(active_persona_id) = uc.persona.active_persona_id.as_ref() {
556                configs.push(format!("Active Persona: {}", active_persona_id));
557            }
558            if !uc.contract.active_contracts.is_empty() {
559                configs
560                    .push(format!("Active Contracts: {}", uc.contract.active_contracts.join(", ")));
561            }
562        }
563
564        // Add reality continuum if no specific configs found
565        if configs.is_empty() {
566            configs.push("Reality Continuum Settings".to_string());
567        }
568
569        configs
570    }
571}
572
573impl Default for DebugAnalyzer {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578
579/// Parsed failure information from test logs
580#[derive(Debug, Default)]
581struct ParsedFailureInfo {
582    method: Option<String>,
583    path: Option<String>,
584    status_code: Option<u16>,
585    error_message: Option<String>,
586}
587
588/// Request for debug analysis
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct DebugRequest {
591    /// Test failure logs
592    pub test_logs: String,
593
594    /// Test name/identifier
595    pub test_name: Option<String>,
596
597    /// Workspace ID for context
598    pub workspace_id: Option<String>,
599}
600
601/// Response from debug analysis
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct DebugResponse {
604    /// Identified root cause
605    pub root_cause: String,
606
607    /// Suggested fixes
608    pub suggestions: Vec<DebugSuggestion>,
609
610    /// Related mock configurations
611    pub related_configs: Vec<String>,
612
613    /// Full failure context (optional, for detailed analysis)
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub context: Option<FailureContext>,
616
617    /// Unified debug context from subsystems (optional)
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub unified_context: Option<UnifiedDebugContext>,
620}
621
622/// Debug suggestion for fixing a test failure
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct DebugSuggestion {
625    /// Suggestion title
626    pub title: String,
627
628    /// Detailed description
629    pub description: String,
630
631    /// Suggested action
632    pub action: String,
633
634    /// Configuration path to update
635    pub config_path: Option<String>,
636
637    /// JSON Patch operation for applying the fix (optional)
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub patch: Option<DebugPatch>,
640
641    /// Linked artifacts (persona IDs, scenario names, contract paths)
642    #[serde(default, skip_serializing_if = "Vec::is_empty")]
643    pub linked_artifacts: Vec<LinkedArtifact>,
644}
645
646/// Linked artifact reference
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct LinkedArtifact {
649    /// Artifact type (persona, scenario, contract, reality)
650    pub artifact_type: String,
651    /// Artifact ID or path
652    pub artifact_id: String,
653    /// Artifact name (optional)
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub artifact_name: Option<String>,
656}
657
658/// JSON Patch operation for applying a debug suggestion
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct DebugPatch {
661    /// Patch operation type: "add", "remove", or "replace"
662    pub op: String,
663
664    /// JSON Pointer path to the field to modify
665    pub path: String,
666
667    /// Value to add or replace (for "add" and "replace" operations)
668    #[serde(skip_serializing_if = "Option::is_none")]
669    pub value: Option<serde_json::Value>,
670
671    /// Source path for "move" or "copy" operations
672    #[serde(skip_serializing_if = "Option::is_none")]
673    pub from: Option<String>,
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use crate::ai_studio::debug_context_integrator::DebugContextIntegrator;
680    use crate::intelligent_behavior::config::BehaviorModelConfig;
681    use serde_json::json;
682
683    fn create_test_config() -> IntelligentBehaviorConfig {
684        IntelligentBehaviorConfig {
685            behavior_model: BehaviorModelConfig {
686                llm_provider: "ollama".to_string(),
687                model: "llama2".to_string(),
688                api_endpoint: Some("http://localhost:11434/api/chat".to_string()),
689                api_key: None,
690                temperature: 0.7,
691                max_tokens: 2000,
692                rules: crate::intelligent_behavior::types::BehaviorRules::default(),
693                seed: None,
694            },
695            ..Default::default()
696        }
697    }
698
699    #[test]
700    fn test_debug_analyzer_new() {
701        let analyzer = DebugAnalyzer::new();
702        // Just verify it can be created
703        let _ = analyzer;
704    }
705
706    #[test]
707    fn test_debug_analyzer_default() {
708        let analyzer = DebugAnalyzer::default();
709        // Just verify it can be created
710        let _ = analyzer;
711    }
712
713    #[test]
714    fn test_debug_analyzer_with_config() {
715        let config = create_test_config();
716        let analyzer = DebugAnalyzer::with_config(config);
717        // Just verify it can be created
718        let _ = analyzer;
719    }
720
721    #[test]
722    fn test_debug_analyzer_with_integrator() {
723        // Create a minimal integrator for testing
724        // Note: This might fail if DebugContextIntegrator::new() requires parameters
725        // In that case, we'll need to adjust the test
726        let integrator = DebugContextIntegrator::new();
727        let analyzer = DebugAnalyzer::with_integrator(integrator);
728        // Just verify it can be created
729        let _ = analyzer;
730    }
731
732    #[test]
733    fn test_debug_analyzer_with_config_and_integrator() {
734        let config = create_test_config();
735        let integrator = DebugContextIntegrator::new();
736        let analyzer = DebugAnalyzer::with_config_and_integrator(config, integrator);
737        // Just verify it can be created
738        let _ = analyzer;
739    }
740
741    #[test]
742    fn test_debug_request_creation() {
743        let request = DebugRequest {
744            test_logs: "GET /api/users 404".to_string(),
745            test_name: Some("test_get_user".to_string()),
746            workspace_id: Some("ws-123".to_string()),
747        };
748
749        assert_eq!(request.test_logs, "GET /api/users 404");
750        assert_eq!(request.test_name, Some("test_get_user".to_string()));
751        assert_eq!(request.workspace_id, Some("ws-123".to_string()));
752    }
753
754    #[test]
755    fn test_debug_request_serialization() {
756        let request = DebugRequest {
757            test_logs: "Error: 500 Internal Server Error".to_string(),
758            test_name: None,
759            workspace_id: None,
760        };
761
762        let json = serde_json::to_string(&request).unwrap();
763        assert!(json.contains("Error: 500"));
764    }
765
766    #[test]
767    fn test_debug_response_creation() {
768        let response = DebugResponse {
769            root_cause: "Authentication failed".to_string(),
770            suggestions: vec![],
771            related_configs: vec!["Persona: admin".to_string()],
772            context: None,
773            unified_context: None,
774        };
775
776        assert_eq!(response.root_cause, "Authentication failed");
777        assert_eq!(response.related_configs.len(), 1);
778    }
779
780    #[test]
781    fn test_debug_response_serialization() {
782        let response = DebugResponse {
783            root_cause: "Root cause".to_string(),
784            suggestions: vec![],
785            related_configs: vec![],
786            context: None,
787            unified_context: None,
788        };
789
790        let json = serde_json::to_string(&response).unwrap();
791        assert!(json.contains("Root cause"));
792    }
793
794    #[test]
795    fn test_debug_suggestion_creation() {
796        let suggestion = DebugSuggestion {
797            title: "Fix authentication".to_string(),
798            description: "Update the auth token".to_string(),
799            action: "Update config".to_string(),
800            config_path: Some("/auth/token".to_string()),
801            patch: None,
802            linked_artifacts: vec![],
803        };
804
805        assert_eq!(suggestion.title, "Fix authentication");
806        assert_eq!(suggestion.config_path, Some("/auth/token".to_string()));
807    }
808
809    #[test]
810    fn test_debug_suggestion_serialization() {
811        let suggestion = DebugSuggestion {
812            title: "Test suggestion".to_string(),
813            description: "Test description".to_string(),
814            action: "Test action".to_string(),
815            config_path: None,
816            patch: None,
817            linked_artifacts: vec![],
818        };
819
820        let json = serde_json::to_string(&suggestion).unwrap();
821        assert!(json.contains("Test suggestion"));
822    }
823
824    #[test]
825    fn test_linked_artifact_creation() {
826        let artifact = LinkedArtifact {
827            artifact_type: "persona".to_string(),
828            artifact_id: "persona-123".to_string(),
829            artifact_name: Some("Admin Persona".to_string()),
830        };
831
832        assert_eq!(artifact.artifact_type, "persona");
833        assert_eq!(artifact.artifact_id, "persona-123");
834        assert_eq!(artifact.artifact_name, Some("Admin Persona".to_string()));
835    }
836
837    #[test]
838    fn test_linked_artifact_serialization() {
839        let artifact = LinkedArtifact {
840            artifact_type: "scenario".to_string(),
841            artifact_id: "scenario-456".to_string(),
842            artifact_name: None,
843        };
844
845        let json = serde_json::to_string(&artifact).unwrap();
846        assert!(json.contains("scenario"));
847        assert!(json.contains("scenario-456"));
848    }
849
850    #[test]
851    fn test_debug_patch_creation() {
852        let patch = DebugPatch {
853            op: "replace".to_string(),
854            path: "/status".to_string(),
855            value: Some(json!("active")),
856            from: None,
857        };
858
859        assert_eq!(patch.op, "replace");
860        assert_eq!(patch.path, "/status");
861        assert!(patch.value.is_some());
862    }
863
864    #[test]
865    fn test_debug_patch_serialization() {
866        let patch = DebugPatch {
867            op: "add".to_string(),
868            path: "/new_field".to_string(),
869            value: Some(json!({"key": "value"})),
870            from: None,
871        };
872
873        let json = serde_json::to_string(&patch).unwrap();
874        assert!(json.contains("add"));
875        assert!(json.contains("new_field"));
876    }
877
878    #[test]
879    fn test_debug_patch_with_from() {
880        let patch = DebugPatch {
881            op: "move".to_string(),
882            path: "/target".to_string(),
883            value: None,
884            from: Some("/source".to_string()),
885        };
886
887        assert_eq!(patch.op, "move");
888        assert_eq!(patch.from, Some("/source".to_string()));
889    }
890
891    #[test]
892    fn test_parsed_failure_info_default() {
893        let info = ParsedFailureInfo::default();
894        assert!(info.method.is_none());
895        assert!(info.path.is_none());
896        assert!(info.status_code.is_none());
897        assert!(info.error_message.is_none());
898    }
899
900    #[test]
901    fn test_debug_request_clone() {
902        let request1 = DebugRequest {
903            test_logs: "GET /api/test 404".to_string(),
904            test_name: Some("test".to_string()),
905            workspace_id: Some("ws-1".to_string()),
906        };
907        let request2 = request1.clone();
908        assert_eq!(request1.test_logs, request2.test_logs);
909    }
910
911    #[test]
912    fn test_debug_request_debug() {
913        let request = DebugRequest {
914            test_logs: "Error occurred".to_string(),
915            test_name: None,
916            workspace_id: None,
917        };
918        let debug_str = format!("{:?}", request);
919        assert!(debug_str.contains("DebugRequest"));
920    }
921
922    #[test]
923    fn test_debug_response_clone() {
924        let response1 = DebugResponse {
925            root_cause: "Root cause".to_string(),
926            suggestions: vec![],
927            related_configs: vec![],
928            context: None,
929            unified_context: None,
930        };
931        let response2 = response1.clone();
932        assert_eq!(response1.root_cause, response2.root_cause);
933    }
934
935    #[test]
936    fn test_debug_response_debug() {
937        let response = DebugResponse {
938            root_cause: "Test root cause".to_string(),
939            suggestions: vec![],
940            related_configs: vec!["config1".to_string()],
941            context: None,
942            unified_context: None,
943        };
944        let debug_str = format!("{:?}", response);
945        assert!(debug_str.contains("DebugResponse"));
946    }
947
948    #[test]
949    fn test_debug_suggestion_clone() {
950        let suggestion1 = DebugSuggestion {
951            title: "Fix issue".to_string(),
952            description: "Description".to_string(),
953            action: "Action".to_string(),
954            config_path: None,
955            patch: None,
956            linked_artifacts: vec![],
957        };
958        let suggestion2 = suggestion1.clone();
959        assert_eq!(suggestion1.title, suggestion2.title);
960    }
961
962    #[test]
963    fn test_debug_suggestion_debug() {
964        let suggestion = DebugSuggestion {
965            title: "Test suggestion".to_string(),
966            description: "Test description".to_string(),
967            action: "Test action".to_string(),
968            config_path: Some("/config/path".to_string()),
969            patch: None,
970            linked_artifacts: vec![],
971        };
972        let debug_str = format!("{:?}", suggestion);
973        assert!(debug_str.contains("DebugSuggestion"));
974    }
975
976    #[test]
977    fn test_linked_artifact_clone() {
978        let artifact1 = LinkedArtifact {
979            artifact_type: "persona".to_string(),
980            artifact_id: "id-1".to_string(),
981            artifact_name: Some("Name".to_string()),
982        };
983        let artifact2 = artifact1.clone();
984        assert_eq!(artifact1.artifact_type, artifact2.artifact_type);
985    }
986
987    #[test]
988    fn test_linked_artifact_debug() {
989        let artifact = LinkedArtifact {
990            artifact_type: "scenario".to_string(),
991            artifact_id: "id-2".to_string(),
992            artifact_name: None,
993        };
994        let debug_str = format!("{:?}", artifact);
995        assert!(debug_str.contains("LinkedArtifact"));
996    }
997
998    #[test]
999    fn test_debug_patch_clone() {
1000        let patch1 = DebugPatch {
1001            op: "replace".to_string(),
1002            path: "/path".to_string(),
1003            value: Some(json!("value")),
1004            from: None,
1005        };
1006        let patch2 = patch1.clone();
1007        assert_eq!(patch1.op, patch2.op);
1008    }
1009
1010    #[test]
1011    fn test_debug_patch_debug() {
1012        let patch = DebugPatch {
1013            op: "add".to_string(),
1014            path: "/new".to_string(),
1015            value: None,
1016            from: Some("/old".to_string()),
1017        };
1018        let debug_str = format!("{:?}", patch);
1019        assert!(debug_str.contains("DebugPatch"));
1020    }
1021
1022    #[test]
1023    fn test_parsed_failure_info_creation() {
1024        let info = ParsedFailureInfo {
1025            method: Some("POST".to_string()),
1026            path: Some("/api/users".to_string()),
1027            status_code: Some(500),
1028            error_message: Some("Internal error".to_string()),
1029        };
1030        assert_eq!(info.method, Some("POST".to_string()));
1031        assert_eq!(info.status_code, Some(500));
1032    }
1033
1034    #[test]
1035    fn test_debug_request_with_all_fields() {
1036        let request = DebugRequest {
1037            test_logs: "POST /api/users 201\nResponse: {\"id\": 1}".to_string(),
1038            test_name: Some("test_create_user".to_string()),
1039            workspace_id: Some("workspace-123".to_string()),
1040        };
1041        assert!(!request.test_logs.is_empty());
1042        assert!(request.test_name.is_some());
1043        assert!(request.workspace_id.is_some());
1044    }
1045
1046    #[test]
1047    fn test_debug_response_with_all_fields() {
1048        let suggestion = DebugSuggestion {
1049            title: "Fix auth".to_string(),
1050            description: "Update token".to_string(),
1051            action: "Update config".to_string(),
1052            config_path: Some("/auth/token".to_string()),
1053            patch: Some(DebugPatch {
1054                op: "replace".to_string(),
1055                path: "/auth/token".to_string(),
1056                value: Some(json!("new-token")),
1057                from: None,
1058            }),
1059            linked_artifacts: vec![LinkedArtifact {
1060                artifact_type: "persona".to_string(),
1061                artifact_id: "persona-1".to_string(),
1062                artifact_name: Some("Admin".to_string()),
1063            }],
1064        };
1065        let response = DebugResponse {
1066            root_cause: "Authentication failed".to_string(),
1067            suggestions: vec![suggestion],
1068            related_configs: vec!["Persona: admin".to_string(), "Scenario: auth".to_string()],
1069            context: None,
1070            unified_context: None,
1071        };
1072        assert_eq!(response.suggestions.len(), 1);
1073        assert_eq!(response.related_configs.len(), 2);
1074    }
1075
1076    #[test]
1077    fn test_debug_suggestion_with_patch() {
1078        let patch = DebugPatch {
1079            op: "replace".to_string(),
1080            path: "/status".to_string(),
1081            value: Some(json!("active")),
1082            from: None,
1083        };
1084        let suggestion = DebugSuggestion {
1085            title: "Update status".to_string(),
1086            description: "Change status to active".to_string(),
1087            action: "Apply patch".to_string(),
1088            config_path: Some("/status".to_string()),
1089            patch: Some(patch.clone()),
1090            linked_artifacts: vec![],
1091        };
1092        assert!(suggestion.patch.is_some());
1093        assert_eq!(suggestion.patch.unwrap().op, "replace");
1094    }
1095
1096    #[test]
1097    fn test_debug_patch_all_operations() {
1098        let operations = vec!["add", "remove", "replace", "move", "copy"];
1099        for op in operations {
1100            let patch = DebugPatch {
1101                op: op.to_string(),
1102                path: "/test".to_string(),
1103                value: Some(json!("value")),
1104                from: None,
1105            };
1106            assert_eq!(patch.op, op);
1107        }
1108    }
1109
1110    #[test]
1111    fn test_linked_artifact_with_name() {
1112        let artifact = LinkedArtifact {
1113            artifact_type: "persona".to_string(),
1114            artifact_id: "persona-123".to_string(),
1115            artifact_name: Some("Admin Persona".to_string()),
1116        };
1117        assert_eq!(artifact.artifact_type, "persona");
1118        assert!(artifact.artifact_name.is_some());
1119    }
1120
1121    #[test]
1122    fn test_linked_artifact_without_name() {
1123        let artifact = LinkedArtifact {
1124            artifact_type: "scenario".to_string(),
1125            artifact_id: "scenario-456".to_string(),
1126            artifact_name: None,
1127        };
1128        assert_eq!(artifact.artifact_type, "scenario");
1129        assert!(artifact.artifact_name.is_none());
1130    }
1131
1132    #[test]
1133    fn test_parsed_failure_info_with_all_fields() {
1134        let info = ParsedFailureInfo {
1135            method: Some("PUT".to_string()),
1136            path: Some("/api/users/123".to_string()),
1137            status_code: Some(422),
1138            error_message: Some("Validation failed: email is required".to_string()),
1139        };
1140        assert_eq!(info.method, Some("PUT".to_string()));
1141        assert_eq!(info.path, Some("/api/users/123".to_string()));
1142        assert_eq!(info.status_code, Some(422));
1143        assert!(info.error_message.is_some());
1144    }
1145}