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 seed: None,
288 };
289
290 let response = self.llm_client.generate(&llm_request).await?;
292
293 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 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 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 if let Some(uc) = unified_context {
333 for suggestion in &mut suggestions {
334 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 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 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 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 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 if let Some(config_path) = &suggestion.config_path {
406 let patch = self.create_patch_for_suggestion(suggestion, config_path, context)?;
408 suggestion.patch = patch;
409 }
410 }
411 Ok(())
412 }
413
414 fn create_patch_for_suggestion(
416 &self,
417 suggestion: &DebugSuggestion,
418 config_path: &str,
419 context: &FailureContext,
420 ) -> Result<Option<DebugPatch>> {
421 let patch = if suggestion.action.contains("add") || suggestion.action.contains("Add") {
423 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 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 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 fn build_patch_path(&self, config_path: &str, suggestion_title: &str) -> String {
453 let mut path = config_path.replace('.', "/");
457 if !path.starts_with('/') {
458 path = format!("/{}", path);
459 }
460
461 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 fn infer_patch_value(
475 &self,
476 suggestion: &DebugSuggestion,
477 context: &FailureContext,
478 ) -> Option<serde_json::Value> {
479 if suggestion.title.contains("422") || suggestion.description.contains("422") {
481 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 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 Some(serde_json::json!({
505 "traits": {},
506 "domain": "general"
507 }))
508 } else {
509 Some(serde_json::json!({
511 "enabled": true
512 }))
513 }
514 }
515
516 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 if context.contract_validation.is_some() {
526 configs.push("Contract Validation".to_string());
527 }
528
529 if !context.behavioral_rules.is_empty() {
531 configs.push("Persona Configuration".to_string());
532 }
533
534 if !context.chaos_configs.is_empty() {
536 configs.push("Chaos Configuration".to_string());
537 }
538
539 if !context.consistency_rules.is_empty() {
541 configs.push("Consistency Rules".to_string());
542 }
543
544 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct DebugRequest {
591 pub test_logs: String,
593
594 pub test_name: Option<String>,
596
597 pub workspace_id: Option<String>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct DebugResponse {
604 pub root_cause: String,
606
607 pub suggestions: Vec<DebugSuggestion>,
609
610 pub related_configs: Vec<String>,
612
613 #[serde(skip_serializing_if = "Option::is_none")]
615 pub context: Option<FailureContext>,
616
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub unified_context: Option<UnifiedDebugContext>,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct DebugSuggestion {
625 pub title: String,
627
628 pub description: String,
630
631 pub action: String,
633
634 pub config_path: Option<String>,
636
637 #[serde(skip_serializing_if = "Option::is_none")]
639 pub patch: Option<DebugPatch>,
640
641 #[serde(default, skip_serializing_if = "Vec::is_empty")]
643 pub linked_artifacts: Vec<LinkedArtifact>,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct LinkedArtifact {
649 pub artifact_type: String,
651 pub artifact_id: String,
653 #[serde(skip_serializing_if = "Option::is_none")]
655 pub artifact_name: Option<String>,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct DebugPatch {
661 pub op: String,
663
664 pub path: String,
666
667 #[serde(skip_serializing_if = "Option::is_none")]
669 pub value: Option<serde_json::Value>,
670
671 #[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 let _ = analyzer;
704 }
705
706 #[test]
707 fn test_debug_analyzer_default() {
708 let analyzer = DebugAnalyzer::default();
709 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 let _ = analyzer;
719 }
720
721 #[test]
722 fn test_debug_analyzer_with_integrator() {
723 let integrator = DebugContextIntegrator::new();
727 let analyzer = DebugAnalyzer::with_integrator(integrator);
728 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 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}