1use 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
19pub struct DebugAnalyzer {
21 context_collector: FailureContextCollector,
23 narrative_generator: FailureNarrativeGenerator,
25 llm_client: LlmClient,
27 context_integrator: Option<DebugContextIntegrator>,
29}
30
31impl DebugAnalyzer {
32 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 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 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 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 pub async fn analyze(&self, request: &DebugRequest) -> Result<DebugResponse> {
84 let failure_info = self.parse_test_logs(&request.test_logs)?;
86
87 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 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 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 let mut suggestions = self
112 .generate_suggestions(&context, &narrative, unified_context.as_ref())
113 .await?;
114
115 self.generate_patches(&mut suggestions, &context, &narrative, unified_context.as_ref())?;
117
118 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 fn parse_test_logs(&self, logs: &str) -> Result<ParsedFailureInfo> {
132 let mut info = ParsedFailureInfo::default();
135
136 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 for line in logs.lines() {
146 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 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 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 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 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 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 let response = self.llm_client.generate(&llm_request).await?;
291
292 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 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 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 if let Some(uc) = unified_context {
332 for suggestion in &mut suggestions {
333 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 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 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 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 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 if let Some(config_path) = &suggestion.config_path {
405 let patch = self.create_patch_for_suggestion(suggestion, config_path, context)?;
407 suggestion.patch = patch;
408 }
409 }
410 Ok(())
411 }
412
413 fn create_patch_for_suggestion(
415 &self,
416 suggestion: &DebugSuggestion,
417 config_path: &str,
418 context: &FailureContext,
419 ) -> Result<Option<DebugPatch>> {
420 let patch = if suggestion.action.contains("add") || suggestion.action.contains("Add") {
422 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 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 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 fn build_patch_path(&self, config_path: &str, suggestion_title: &str) -> String {
452 let mut path = config_path.replace('.', "/");
456 if !path.starts_with('/') {
457 path = format!("/{}", path);
458 }
459
460 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 fn infer_patch_value(
474 &self,
475 suggestion: &DebugSuggestion,
476 context: &FailureContext,
477 ) -> Option<serde_json::Value> {
478 if suggestion.title.contains("422") || suggestion.description.contains("422") {
480 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 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 Some(serde_json::json!({
504 "traits": {},
505 "domain": "general"
506 }))
507 } else {
508 Some(serde_json::json!({
510 "enabled": true
511 }))
512 }
513 }
514
515 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 if context.contract_validation.is_some() {
525 configs.push("Contract Validation".to_string());
526 }
527
528 if !context.behavioral_rules.is_empty() {
530 configs.push("Persona Configuration".to_string());
531 }
532
533 if !context.chaos_configs.is_empty() {
535 configs.push("Chaos Configuration".to_string());
536 }
537
538 if !context.consistency_rules.is_empty() {
540 configs.push("Consistency Rules".to_string());
541 }
542
543 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct DebugRequest {
590 pub test_logs: String,
592
593 pub test_name: Option<String>,
595
596 pub workspace_id: Option<String>,
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct DebugResponse {
603 pub root_cause: String,
605
606 pub suggestions: Vec<DebugSuggestion>,
608
609 pub related_configs: Vec<String>,
611
612 #[serde(skip_serializing_if = "Option::is_none")]
614 pub context: Option<FailureContext>,
615
616 #[serde(skip_serializing_if = "Option::is_none")]
618 pub unified_context: Option<UnifiedDebugContext>,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct DebugSuggestion {
624 pub title: String,
626
627 pub description: String,
629
630 pub action: String,
632
633 pub config_path: Option<String>,
635
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub patch: Option<DebugPatch>,
639
640 #[serde(default, skip_serializing_if = "Vec::is_empty")]
642 pub linked_artifacts: Vec<LinkedArtifact>,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct LinkedArtifact {
648 pub artifact_type: String,
650 pub artifact_id: String,
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub artifact_name: Option<String>,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize)]
659pub struct DebugPatch {
660 pub op: String,
662
663 pub path: String,
665
666 #[serde(skip_serializing_if = "Option::is_none")]
668 pub value: Option<serde_json::Value>,
669
670 #[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 let _ = analyzer;
702 }
703
704 #[test]
705 fn test_debug_analyzer_default() {
706 let analyzer = DebugAnalyzer::default();
707 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 let _ = analyzer;
717 }
718
719 #[test]
720 fn test_debug_analyzer_with_integrator() {
721 let integrator = DebugContextIntegrator::new();
725 let analyzer = DebugAnalyzer::with_integrator(integrator);
726 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 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}