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