1use super::common::{build_server_config, load_server_from_config};
6use anyhow::{Context, Result};
7use mcp_execution_core::cli::{ExitCode, OutputFormat};
8use mcp_execution_introspector::{Introspector, ServerInfo, ToolInfo};
9use serde::Serialize;
10use tracing::{debug, info};
11
12#[derive(Debug, Clone, Serialize)]
37pub struct IntrospectionResult {
38 pub server: ServerMetadata,
40 pub tools: Vec<ToolDisplay>,
42}
43
44#[derive(Debug, Clone, Serialize)]
49pub struct ServerMetadata {
50 pub id: String,
52 pub name: String,
54 pub version: String,
56 pub supports_tools: bool,
58 pub supports_resources: bool,
60 pub supports_prompts: bool,
62}
63
64#[derive(Debug, Clone, Serialize)]
69pub struct ToolDisplay {
70 pub name: String,
72 pub description: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub input_schema: Option<serde_json::Value>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub output_schema: Option<serde_json::Value>,
80}
81
82#[allow(clippy::too_many_arguments)]
166pub async fn run(
167 from_config: Option<String>,
168 server: Option<String>,
169 args: Vec<String>,
170 env: Vec<String>,
171 cwd: Option<String>,
172 http: Option<String>,
173 sse: Option<String>,
174 headers: Vec<String>,
175 detailed: bool,
176 connect_timeout_secs: Option<u64>,
177 discover_timeout_secs: Option<u64>,
178 output_format: OutputFormat,
179) -> Result<ExitCode> {
180 let (server_id, config) = if let Some(config_name) = from_config {
182 debug!(
183 "Loading server configuration from ~/.claude/mcp.json: {}",
184 config_name
185 );
186 load_server_from_config(&config_name)?
187 } else {
188 build_server_config(
189 server,
190 args,
191 env,
192 cwd,
193 http,
194 sse,
195 headers,
196 connect_timeout_secs,
197 discover_timeout_secs,
198 )?
199 };
200
201 info!("Introspecting server: {}", server_id);
202 info!("Transport: {:?}", config.transport());
203 info!("Detailed: {}", detailed);
204 info!("Output format: {}", output_format);
205
206 let mut introspector = Introspector::new();
208
209 let server_info = introspector
211 .discover_server(server_id.clone(), &config)
212 .await
213 .with_context(|| {
214 format!(
215 "failed to connect to server '{server_id}' - ensure the server is installed and accessible"
216 )
217 })?;
218
219 info!(
220 "Successfully discovered {} tools from server",
221 server_info.tools.len()
222 );
223
224 let result = build_result(&server_info, detailed);
226
227 let formatted = crate::formatters::format_output(&result, output_format)
229 .context("failed to format introspection results")?;
230
231 println!("{formatted}");
232
233 Ok(ExitCode::SUCCESS)
234}
235
236#[must_use]
269pub fn build_result(server_info: &ServerInfo, detailed: bool) -> IntrospectionResult {
270 let server = ServerMetadata {
271 id: server_info.id.as_str().to_string(),
272 name: server_info.name.clone(),
273 version: server_info.version.clone(),
274 supports_tools: server_info.capabilities.supports_tools,
275 supports_resources: server_info.capabilities.supports_resources,
276 supports_prompts: server_info.capabilities.supports_prompts,
277 };
278
279 let tools = server_info
280 .tools
281 .iter()
282 .map(|tool| build_tool_metadata(tool, detailed))
283 .collect();
284
285 IntrospectionResult { server, tools }
286}
287
288fn build_tool_metadata(tool_info: &ToolInfo, detailed: bool) -> ToolDisplay {
297 ToolDisplay {
298 name: tool_info.name.as_str().to_string(),
299 description: tool_info.description.clone(),
300 input_schema: if detailed {
301 Some(tool_info.input_schema.clone())
302 } else {
303 None
304 },
305 output_schema: if detailed {
306 tool_info.output_schema.clone()
307 } else {
308 None
309 },
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use mcp_execution_core::{ServerId, ToolName};
317 use mcp_execution_introspector::ServerCapabilities;
318 use serde_json::json;
319
320 #[test]
321 fn test_build_result_basic() {
322 let server_info = ServerInfo {
323 id: ServerId::new("test-server"),
324 name: "Test Server".to_string(),
325 version: "1.0.0".to_string(),
326 tools: vec![],
327 capabilities: ServerCapabilities {
328 supports_tools: true,
329 supports_resources: false,
330 supports_prompts: false,
331 },
332 };
333
334 let result = build_result(&server_info, false);
335
336 assert_eq!(result.server.id, "test-server");
337 assert_eq!(result.server.name, "Test Server");
338 assert_eq!(result.server.version, "1.0.0");
339 assert!(result.server.supports_tools);
340 assert!(!result.server.supports_resources);
341 assert!(!result.server.supports_prompts);
342 assert_eq!(result.tools.len(), 0);
343 }
344
345 #[test]
346 fn test_build_result_with_tools_not_detailed() {
347 let server_info = ServerInfo {
348 id: ServerId::new("test"),
349 name: "Test".to_string(),
350 version: "1.0.0".to_string(),
351 tools: vec![
352 ToolInfo {
353 name: ToolName::new("tool1"),
354 description: "First tool".to_string(),
355 input_schema: json!({"type": "object"}),
356 output_schema: None,
357 },
358 ToolInfo {
359 name: ToolName::new("tool2"),
360 description: "Second tool".to_string(),
361 input_schema: json!({"type": "string"}),
362 output_schema: Some(json!({"type": "boolean"})),
363 },
364 ],
365 capabilities: ServerCapabilities {
366 supports_tools: true,
367 supports_resources: true,
368 supports_prompts: true,
369 },
370 };
371
372 let result = build_result(&server_info, false);
373
374 assert_eq!(result.tools.len(), 2);
375 assert_eq!(result.tools[0].name, "tool1");
376 assert_eq!(result.tools[0].description, "First tool");
377 assert!(result.tools[0].input_schema.is_none());
378 assert!(result.tools[0].output_schema.is_none());
379
380 assert_eq!(result.tools[1].name, "tool2");
381 assert_eq!(result.tools[1].description, "Second tool");
382 assert!(result.tools[1].input_schema.is_none());
383 assert!(result.tools[1].output_schema.is_none());
384 }
385
386 #[test]
387 fn test_build_result_with_tools_detailed() {
388 let server_info = ServerInfo {
389 id: ServerId::new("test"),
390 name: "Test".to_string(),
391 version: "1.0.0".to_string(),
392 tools: vec![
393 ToolInfo {
394 name: ToolName::new("tool1"),
395 description: "First tool".to_string(),
396 input_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
397 output_schema: None,
398 },
399 ToolInfo {
400 name: ToolName::new("tool2"),
401 description: "Second tool".to_string(),
402 input_schema: json!({"type": "string"}),
403 output_schema: Some(json!({"type": "boolean"})),
404 },
405 ],
406 capabilities: ServerCapabilities {
407 supports_tools: true,
408 supports_resources: false,
409 supports_prompts: false,
410 },
411 };
412
413 let result = build_result(&server_info, true);
414
415 assert_eq!(result.tools.len(), 2);
416
417 assert_eq!(result.tools[0].name, "tool1");
419 assert!(result.tools[0].input_schema.is_some());
420 assert_eq!(
421 result.tools[0].input_schema.as_ref().unwrap()["type"],
422 "object"
423 );
424 assert!(result.tools[0].output_schema.is_none());
425
426 assert_eq!(result.tools[1].name, "tool2");
428 assert!(result.tools[1].input_schema.is_some());
429 assert_eq!(
430 result.tools[1].input_schema.as_ref().unwrap()["type"],
431 "string"
432 );
433 assert!(result.tools[1].output_schema.is_some());
434 assert_eq!(
435 result.tools[1].output_schema.as_ref().unwrap()["type"],
436 "boolean"
437 );
438 }
439
440 #[test]
441 fn test_build_tool_metadata_not_detailed() {
442 let tool_info = ToolInfo {
443 name: ToolName::new("send_message"),
444 description: "Sends a message".to_string(),
445 input_schema: json!({"type": "object"}),
446 output_schema: Some(json!({"type": "string"})),
447 };
448
449 let metadata = build_tool_metadata(&tool_info, false);
450
451 assert_eq!(metadata.name, "send_message");
452 assert_eq!(metadata.description, "Sends a message");
453 assert!(metadata.input_schema.is_none());
454 assert!(metadata.output_schema.is_none());
455 }
456
457 #[test]
458 fn test_build_tool_metadata_detailed() {
459 let tool_info = ToolInfo {
460 name: ToolName::new("send_message"),
461 description: "Sends a message".to_string(),
462 input_schema: json!({
463 "type": "object",
464 "properties": {
465 "chat_id": {"type": "string"},
466 "text": {"type": "string"}
467 }
468 }),
469 output_schema: Some(json!({"type": "string"})),
470 };
471
472 let metadata = build_tool_metadata(&tool_info, true);
473
474 assert_eq!(metadata.name, "send_message");
475 assert_eq!(metadata.description, "Sends a message");
476 assert!(metadata.input_schema.is_some());
477 assert_eq!(metadata.input_schema.as_ref().unwrap()["type"], "object");
478 assert!(metadata.output_schema.is_some());
479 assert_eq!(metadata.output_schema.as_ref().unwrap()["type"], "string");
480 }
481
482 #[test]
483 fn test_introspection_result_serialization() {
484 let result = IntrospectionResult {
485 server: ServerMetadata {
486 id: "test".to_string(),
487 name: "Test Server".to_string(),
488 version: "1.0.0".to_string(),
489 supports_tools: true,
490 supports_resources: false,
491 supports_prompts: false,
492 },
493 tools: vec![ToolDisplay {
494 name: "test_tool".to_string(),
495 description: "A test tool".to_string(),
496 input_schema: None,
497 output_schema: None,
498 }],
499 };
500
501 let json = serde_json::to_string(&result).unwrap();
502 assert!(json.contains("Test Server"));
503 assert!(json.contains("test_tool"));
504
505 assert!(!json.contains("input_schema"));
507 assert!(!json.contains("output_schema"));
508 }
509
510 #[test]
511 fn test_introspection_result_serialization_with_schemas() {
512 let result = IntrospectionResult {
513 server: ServerMetadata {
514 id: "test".to_string(),
515 name: "Test Server".to_string(),
516 version: "1.0.0".to_string(),
517 supports_tools: true,
518 supports_resources: false,
519 supports_prompts: false,
520 },
521 tools: vec![ToolDisplay {
522 name: "test_tool".to_string(),
523 description: "A test tool".to_string(),
524 input_schema: Some(json!({"type": "object"})),
525 output_schema: Some(json!({"type": "string"})),
526 }],
527 };
528
529 let json = serde_json::to_string(&result).unwrap();
530 assert!(json.contains("input_schema"));
531 assert!(json.contains("output_schema"));
532 assert!(json.contains("\"type\":\"object\""));
533 assert!(json.contains("\"type\":\"string\""));
534 }
535
536 #[tokio::test]
537 async fn test_run_server_connection_failure() {
538 let result = run(
539 None,
540 Some("nonexistent-server-xyz".to_string()),
541 vec![],
542 vec![],
543 None,
544 None,
545 None,
546 vec![],
547 false,
548 None,
549 None,
550 OutputFormat::Json,
551 )
552 .await;
553
554 assert!(result.is_err());
555 let err_msg = result.unwrap_err().to_string();
556 assert!(err_msg.contains("failed to connect to server"));
557 }
558
559 #[test]
562 fn test_server_metadata_all_capabilities() {
563 let metadata = ServerMetadata {
564 id: "test".to_string(),
565 name: "Test".to_string(),
566 version: "2.0.0".to_string(),
567 supports_tools: true,
568 supports_resources: true,
569 supports_prompts: true,
570 };
571
572 assert!(metadata.supports_tools);
573 assert!(metadata.supports_resources);
574 assert!(metadata.supports_prompts);
575 }
576
577 #[test]
578 fn test_server_metadata_no_capabilities() {
579 let metadata = ServerMetadata {
580 id: "test".to_string(),
581 name: "Test".to_string(),
582 version: "1.0.0".to_string(),
583 supports_tools: false,
584 supports_resources: false,
585 supports_prompts: false,
586 };
587
588 assert!(!metadata.supports_tools);
589 assert!(!metadata.supports_resources);
590 assert!(!metadata.supports_prompts);
591 }
592
593 #[test]
594 fn test_tool_metadata_empty_description() {
595 let metadata = ToolDisplay {
596 name: "tool".to_string(),
597 description: String::new(),
598 input_schema: None,
599 output_schema: None,
600 };
601
602 assert_eq!(metadata.description, "");
603 }
604
605 #[test]
606 fn test_build_result_preserves_tool_order() {
607 let server_info = ServerInfo {
608 id: ServerId::new("test"),
609 name: "Test".to_string(),
610 version: "1.0.0".to_string(),
611 tools: vec![
612 ToolInfo {
613 name: ToolName::new("alpha"),
614 description: "A".to_string(),
615 input_schema: json!({}),
616 output_schema: None,
617 },
618 ToolInfo {
619 name: ToolName::new("beta"),
620 description: "B".to_string(),
621 input_schema: json!({}),
622 output_schema: None,
623 },
624 ToolInfo {
625 name: ToolName::new("gamma"),
626 description: "C".to_string(),
627 input_schema: json!({}),
628 output_schema: None,
629 },
630 ],
631 capabilities: ServerCapabilities {
632 supports_tools: true,
633 supports_resources: false,
634 supports_prompts: false,
635 },
636 };
637
638 let result = build_result(&server_info, false);
639
640 assert_eq!(result.tools.len(), 3);
641 assert_eq!(result.tools[0].name, "alpha");
642 assert_eq!(result.tools[1].name, "beta");
643 assert_eq!(result.tools[2].name, "gamma");
644 }
645
646 #[tokio::test]
647 async fn test_run_with_text_format() {
648 let result = run(
650 None,
651 Some("nonexistent-server".to_string()),
652 vec![],
653 vec![],
654 None,
655 None,
656 None,
657 vec![],
658 false,
659 None,
660 None,
661 OutputFormat::Text,
662 )
663 .await;
664
665 assert!(result.is_err());
667 }
668
669 #[tokio::test]
670 async fn test_run_with_pretty_format() {
671 let result = run(
673 None,
674 Some("nonexistent-server".to_string()),
675 vec![],
676 vec![],
677 None,
678 None,
679 None,
680 vec![],
681 false,
682 None,
683 None,
684 OutputFormat::Pretty,
685 )
686 .await;
687
688 assert!(result.is_err());
690 }
691
692 #[tokio::test]
693 async fn test_run_with_detailed_mode() {
694 let result = run(
696 None,
697 Some("nonexistent-server".to_string()),
698 vec![],
699 vec![],
700 None,
701 None,
702 None,
703 vec![],
704 true, None,
706 None,
707 OutputFormat::Json,
708 )
709 .await;
710
711 assert!(result.is_err());
712 }
713
714 #[tokio::test]
715 async fn test_run_http_transport() {
716 let result = run(
718 None,
719 None,
720 vec![],
721 vec![],
722 None,
723 Some("https://localhost:99999/invalid".to_string()),
724 None,
725 vec!["Authorization=Bearer test".to_string()],
726 false,
727 None,
728 None,
729 OutputFormat::Json,
730 )
731 .await;
732
733 assert!(result.is_err());
734 let err_msg = result.unwrap_err().to_string();
735 assert!(err_msg.contains("failed to connect to server"));
736 }
737
738 #[tokio::test]
739 async fn test_run_sse_transport() {
740 let result = run(
742 None,
743 None,
744 vec![],
745 vec![],
746 None,
747 None,
748 Some("https://localhost:99999/sse".to_string()),
749 vec!["X-API-Key=test-key".to_string()],
750 false,
751 None,
752 None,
753 OutputFormat::Json,
754 )
755 .await;
756
757 assert!(result.is_err());
758 let err_msg = result.unwrap_err().to_string();
759 assert!(err_msg.contains("failed to connect to server"));
760 }
761
762 #[tokio::test]
763 async fn test_run_all_output_formats() {
764 for format in [OutputFormat::Json, OutputFormat::Text, OutputFormat::Pretty] {
766 let result = run(
767 None,
768 Some("nonexistent".to_string()),
769 vec![],
770 vec![],
771 None,
772 None,
773 None,
774 vec![],
775 false,
776 None,
777 None,
778 format,
779 )
780 .await;
781
782 assert!(result.is_err());
783 }
784 }
785
786 #[tokio::test]
787 async fn test_run_detailed_with_all_formats() {
788 for format in [OutputFormat::Json, OutputFormat::Text, OutputFormat::Pretty] {
790 let result = run(
791 None,
792 Some("nonexistent".to_string()),
793 vec![],
794 vec![],
795 None,
796 None,
797 None,
798 vec![],
799 true, None,
801 None,
802 format,
803 )
804 .await;
805
806 assert!(result.is_err());
807 }
808 }
809
810 #[test]
811 fn test_build_result_empty_tools() {
812 let server_info = ServerInfo {
813 id: ServerId::new("empty"),
814 name: "Empty Server".to_string(),
815 version: "0.1.0".to_string(),
816 tools: vec![],
817 capabilities: ServerCapabilities {
818 supports_tools: false,
819 supports_resources: false,
820 supports_prompts: false,
821 },
822 };
823
824 let result = build_result(&server_info, false);
825
826 assert_eq!(result.server.name, "Empty Server");
827 assert_eq!(result.tools.len(), 0);
828 assert!(!result.server.supports_tools);
829 }
830
831 #[test]
832 fn test_build_result_many_tools() {
833 let tools: Vec<ToolInfo> = (0..100)
835 .map(|i| ToolInfo {
836 name: ToolName::new(&format!("tool_{i}")),
837 description: format!("Tool number {i}"),
838 input_schema: json!({"type": "object"}),
839 output_schema: Some(json!({"type": "string"})),
840 })
841 .collect();
842
843 let server_info = ServerInfo {
844 id: ServerId::new("many-tools"),
845 name: "Server with many tools".to_string(),
846 version: "1.0.0".to_string(),
847 tools,
848 capabilities: ServerCapabilities {
849 supports_tools: true,
850 supports_resources: true,
851 supports_prompts: true,
852 },
853 };
854
855 let result = build_result(&server_info, true);
856
857 assert_eq!(result.tools.len(), 100);
858 assert_eq!(result.tools[0].name, "tool_0");
859 assert_eq!(result.tools[99].name, "tool_99");
860 assert!(result.tools[0].input_schema.is_some());
862 assert!(result.tools[0].output_schema.is_some());
863 }
864
865 #[test]
866 fn test_build_tool_metadata_complex_schema() {
867 let tool_info = ToolInfo {
868 name: ToolName::new("complex_tool"),
869 description: "Tool with complex schema".to_string(),
870 input_schema: json!({
871 "type": "object",
872 "properties": {
873 "name": {"type": "string", "minLength": 1},
874 "age": {"type": "integer", "minimum": 0},
875 "tags": {
876 "type": "array",
877 "items": {"type": "string"}
878 }
879 },
880 "required": ["name"]
881 }),
882 output_schema: Some(json!({
883 "type": "object",
884 "properties": {
885 "success": {"type": "boolean"},
886 "message": {"type": "string"}
887 }
888 })),
889 };
890
891 let metadata = build_tool_metadata(&tool_info, true);
892
893 assert_eq!(metadata.name, "complex_tool");
894 assert!(metadata.input_schema.is_some());
895 assert!(metadata.output_schema.is_some());
896
897 let input = metadata.input_schema.as_ref().unwrap();
898 assert_eq!(input["type"], "object");
899 assert!(input["properties"]["name"].is_object());
900 assert!(input["properties"]["tags"]["items"].is_object());
901 }
902
903 #[test]
904 fn test_introspection_result_clone() {
905 let result = IntrospectionResult {
906 server: ServerMetadata {
907 id: "test".to_string(),
908 name: "Test".to_string(),
909 version: "1.0.0".to_string(),
910 supports_tools: true,
911 supports_resources: false,
912 supports_prompts: false,
913 },
914 tools: vec![],
915 };
916
917 let cloned = result.clone();
919 assert_eq!(cloned.server.id, result.server.id);
920 assert_eq!(cloned.server.name, result.server.name);
921 }
922
923 #[test]
924 fn test_server_metadata_serialization_all_fields() {
925 let metadata = ServerMetadata {
926 id: "test-id".to_string(),
927 name: "Test Server".to_string(),
928 version: "2.1.0".to_string(),
929 supports_tools: true,
930 supports_resources: true,
931 supports_prompts: true,
932 };
933
934 let json = serde_json::to_value(&metadata).unwrap();
935
936 assert_eq!(json["id"], "test-id");
937 assert_eq!(json["name"], "Test Server");
938 assert_eq!(json["version"], "2.1.0");
939 assert_eq!(json["supports_tools"], true);
940 assert_eq!(json["supports_resources"], true);
941 assert_eq!(json["supports_prompts"], true);
942 }
943
944 #[test]
945 fn test_tool_metadata_serialization_without_schemas() {
946 let metadata = ToolDisplay {
947 name: "simple_tool".to_string(),
948 description: "A simple tool".to_string(),
949 input_schema: None,
950 output_schema: None,
951 };
952
953 let json = serde_json::to_string(&metadata).unwrap();
954
955 assert!(!json.contains("input_schema"));
957 assert!(!json.contains("output_schema"));
958 assert!(json.contains("simple_tool"));
959 assert!(json.contains("A simple tool"));
960 }
961
962 #[test]
963 fn test_tool_metadata_long_description() {
964 let long_description = "A".repeat(1000);
965 let metadata = ToolDisplay {
966 name: "tool".to_string(),
967 description: long_description.clone(),
968 input_schema: None,
969 output_schema: None,
970 };
971
972 assert_eq!(metadata.description.len(), 1000);
974 let json = serde_json::to_string(&metadata).unwrap();
975 assert!(json.contains(&long_description));
976 }
977
978 #[test]
979 fn test_build_result_mixed_capabilities() {
980 let server_info = ServerInfo {
981 id: ServerId::new("mixed"),
982 name: "Mixed Server".to_string(),
983 version: "1.0.0".to_string(),
984 tools: vec![ToolInfo {
985 name: ToolName::new("tool1"),
986 description: "First".to_string(),
987 input_schema: json!({}),
988 output_schema: None,
989 }],
990 capabilities: ServerCapabilities {
991 supports_tools: true,
992 supports_resources: true,
993 supports_prompts: false, },
995 };
996
997 let result = build_result(&server_info, false);
998
999 assert!(result.server.supports_tools);
1000 assert!(result.server.supports_resources);
1001 assert!(!result.server.supports_prompts);
1002 }
1003
1004 #[tokio::test]
1005 async fn test_run_from_config_not_found() {
1006 let result = run(
1007 Some("nonexistent-server-xyz".to_string()),
1008 None,
1009 vec![],
1010 vec![],
1011 None,
1012 None,
1013 None,
1014 vec![],
1015 false,
1016 None,
1017 None,
1018 OutputFormat::Json,
1019 )
1020 .await;
1021
1022 assert!(result.is_err());
1023 let err_msg = result.unwrap_err().to_string();
1024 assert!(
1025 err_msg.contains("not found in")
1026 || err_msg.contains("failed to read MCP config")
1027 || err_msg.contains("mcp.json"),
1028 "Expected config-related error, got: {err_msg}"
1029 );
1030 }
1031
1032 #[tokio::test]
1033 async fn test_run_from_config_takes_priority() {
1034 let result = run(
1037 Some("test-server".to_string()),
1038 None, vec![],
1040 vec![],
1041 None,
1042 None,
1043 None,
1044 vec![],
1045 false,
1046 None,
1047 None,
1048 OutputFormat::Json,
1049 )
1050 .await;
1051
1052 assert!(result.is_err());
1054 let err_msg = result.unwrap_err().to_string();
1055 assert!(
1057 err_msg.contains("MCP config") || err_msg.contains("test-server"),
1058 "Should attempt config loading: {err_msg}"
1059 );
1060 }
1061
1062 #[tokio::test]
1063 async fn test_run_manual_mode_backward_compatible() {
1064 let result = run(
1066 None, Some("test-server-direct".to_string()),
1068 vec![],
1069 vec![],
1070 None,
1071 None,
1072 None,
1073 vec![],
1074 false,
1075 None,
1076 None,
1077 OutputFormat::Json,
1078 )
1079 .await;
1080
1081 assert!(result.is_err());
1082 let err_msg = result.unwrap_err().to_string();
1083 assert!(
1085 err_msg.contains("failed to connect") || err_msg.contains("test-server-direct"),
1086 "Should try direct connection: {err_msg}"
1087 );
1088 }
1089
1090 #[tokio::test]
1091 async fn test_run_zero_connect_timeout_override_rejected_by_validation() {
1092 let result = run(
1095 None,
1096 Some("nonexistent-server-timeout-test".to_string()),
1097 vec![],
1098 vec![],
1099 None,
1100 None,
1101 None,
1102 vec![],
1103 false,
1104 Some(0),
1105 None,
1106 OutputFormat::Json,
1107 )
1108 .await;
1109
1110 assert!(result.is_err());
1111 let err = result.unwrap_err();
1112 let chain_msg = err
1113 .chain()
1114 .map(ToString::to_string)
1115 .collect::<Vec<_>>()
1116 .join(" | ");
1117 assert!(
1118 chain_msg.contains("greater than zero"),
1119 "expected connect_timeout validation error in the error chain, got: {chain_msg}"
1120 );
1121 }
1122
1123 #[tokio::test]
1124 async fn test_run_with_valid_timeout_overrides_reaches_connection_attempt() {
1125 let result = run(
1127 None,
1128 Some("nonexistent-server-timeout-test-2".to_string()),
1129 vec![],
1130 vec![],
1131 None,
1132 None,
1133 None,
1134 vec![],
1135 false,
1136 Some(5),
1137 Some(90),
1138 OutputFormat::Json,
1139 )
1140 .await;
1141
1142 assert!(result.is_err());
1143 let err_msg = result.unwrap_err().to_string();
1144 assert!(err_msg.contains("failed to connect to server"));
1145 }
1146}