1use rustc_hash::FxHashMap;
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(into = "i32", try_from = "i32")]
45pub enum McpErrorCode {
46 ParseError,
48 InvalidRequest,
50 MethodNotFound,
52 InvalidParams,
54 InternalError,
56 ServerError(i32),
58 Unknown(i32),
60}
61
62impl McpErrorCode {
63 pub fn from_code(code: i32) -> Self {
65 match code {
66 -32700 => Self::ParseError,
67 -32600 => Self::InvalidRequest,
68 -32601 => Self::MethodNotFound,
69 -32602 => Self::InvalidParams,
70 -32603 => Self::InternalError,
71 c if (-32099..=-32000).contains(&c) => Self::ServerError(c),
72 c => Self::Unknown(c),
73 }
74 }
75
76 pub fn code(&self) -> i32 {
78 match self {
79 Self::ParseError => -32700,
80 Self::InvalidRequest => -32600,
81 Self::MethodNotFound => -32601,
82 Self::InvalidParams => -32602,
83 Self::InternalError => -32603,
84 Self::ServerError(c) | Self::Unknown(c) => *c,
85 }
86 }
87
88 pub fn is_client_error(&self) -> bool {
90 matches!(
91 self,
92 Self::ParseError | Self::InvalidRequest | Self::InvalidParams
93 )
94 }
95
96 pub fn is_server_error(&self) -> bool {
98 matches!(
99 self,
100 Self::InternalError | Self::MethodNotFound | Self::ServerError(_)
101 )
102 }
103
104 pub fn description(&self) -> &'static str {
106 match self {
107 Self::ParseError => "Invalid JSON was received",
108 Self::InvalidRequest => "The JSON sent is not a valid Request object",
109 Self::MethodNotFound => "The method does not exist or is not available",
110 Self::InvalidParams => "Invalid method parameter(s)",
111 Self::InternalError => "Internal JSON-RPC error",
112 Self::ServerError(_) => "Server error",
113 Self::Unknown(_) => "Unknown error",
114 }
115 }
116
117 pub fn is_retryable(&self) -> bool {
129 matches!(
130 self,
131 Self::InternalError | Self::ServerError(_) | Self::Unknown(_)
132 )
133 }
134}
135
136impl std::fmt::Display for McpErrorCode {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 write!(f, "{} ({})", self.description(), self.code())
139 }
140}
141
142impl From<McpErrorCode> for i32 {
143 fn from(code: McpErrorCode) -> Self {
144 code.code()
145 }
146}
147
148impl From<i32> for McpErrorCode {
149 fn from(code: i32) -> Self {
150 Self::from_code(code)
151 }
152}
153
154#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
170pub struct McpConfig {
171 #[serde(skip)]
173 pub name: String,
174
175 pub command: String,
177
178 #[serde(default)]
180 pub args: Vec<String>,
181
182 #[serde(default)]
184 pub env: FxHashMap<String, String>,
185
186 #[serde(default)]
188 pub cwd: Option<String>,
189}
190
191impl McpConfig {
192 pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
194 Self {
195 name: name.into(),
196 command: command.into(),
197 args: Vec::new(),
198 env: FxHashMap::default(),
199 cwd: None,
200 }
201 }
202
203 pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
205 self.args.push(arg.into());
206 self
207 }
208
209 pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
211 self.args.extend(args.into_iter().map(Into::into));
212 self
213 }
214
215 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
217 self.env.insert(key.into(), value.into());
218 self
219 }
220
221 pub fn with_cwd(mut self, cwd: impl Into<String>) -> Self {
223 self.cwd = Some(cwd.into());
224 self
225 }
226
227 pub fn expand_env_vars(mut self) -> Result<Self, String> {
251 self.command = shellexpand::full(&self.command)
253 .map_err(|e| format!("Failed to expand command '{}': {}", self.command, e))?
254 .into_owned();
255
256 let mut expanded_args = Vec::with_capacity(self.args.len());
258 for arg in &self.args {
259 let expanded = shellexpand::full(arg)
260 .map_err(|e| format!("Failed to expand arg '{}': {}", arg, e))?
261 .into_owned();
262 expanded_args.push(expanded);
263 }
264 self.args = expanded_args;
265
266 let mut expanded_env = FxHashMap::default();
268 for (key, value) in self.env.drain() {
269 let expanded_value = shellexpand::full(&value)
270 .map_err(|e| format!("Failed to expand env '{}={}': {}", key, value, e))?
271 .into_owned();
272 expanded_env.insert(key, expanded_value);
273 }
274 self.env = expanded_env;
275
276 if let Some(cwd) = self.cwd.as_mut() {
278 *cwd = shellexpand::full(cwd)
279 .map_err(|e| format!("Failed to expand cwd '{}': {}", cwd, e))?
280 .into_owned();
281 }
282
283 Ok(self)
284 }
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
291pub struct ToolCallRequest {
292 pub name: String,
294
295 #[serde(default)]
297 pub arguments: serde_json::Value,
298}
299
300impl ToolCallRequest {
301 pub fn new(name: impl Into<String>) -> Self {
303 Self {
304 name: name.into(),
305 arguments: serde_json::Value::Object(serde_json::Map::new()),
306 }
307 }
308
309 pub fn with_arguments(mut self, args: serde_json::Value) -> Self {
311 self.arguments = args;
312 self
313 }
314}
315
316#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
320pub struct ToolCallResult {
321 pub content: Vec<ContentBlock>,
323
324 #[serde(default)]
326 pub is_error: bool,
327}
328
329impl ToolCallResult {
330 pub fn success(content: Vec<ContentBlock>) -> Self {
332 Self {
333 content,
334 is_error: false,
335 }
336 }
337
338 pub fn error(message: impl Into<String>) -> Self {
340 Self {
341 content: vec![ContentBlock::text(message)],
342 is_error: true,
343 }
344 }
345
346 pub fn text(&self) -> String {
350 self.content
351 .iter()
352 .filter_map(|block| match block {
353 ContentBlock::Text { text } => Some(text.as_str()),
354 _ => None,
355 })
356 .collect::<Vec<_>>()
357 .join("\n")
358 }
359
360 pub fn first_text(&self) -> Option<&str> {
362 self.content.iter().find_map(|block| match block {
363 ContentBlock::Text { text } => Some(text.as_str()),
364 _ => None,
365 })
366 }
367
368 pub fn has_media(&self) -> bool {
370 self.content.iter().any(|b| !b.is_text())
371 }
372
373 pub fn images(&self) -> Vec<&ContentBlock> {
375 self.content.iter().filter(|b| b.is_image()).collect()
376 }
377
378 pub fn audio_blocks(&self) -> Vec<&ContentBlock> {
380 self.content.iter().filter(|b| b.is_audio()).collect()
381 }
382
383 pub fn media_blocks(&self) -> Vec<&ContentBlock> {
385 self.content.iter().filter(|b| !b.is_text()).collect()
386 }
387}
388
389pub use nika_core::mcp::{ContentBlock, ResourceContent};
393
394#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
398pub struct ToolDefinition {
399 pub name: String,
401
402 #[serde(default)]
404 pub description: Option<String>,
405
406 #[serde(default, rename = "inputSchema")]
408 pub input_schema: Option<serde_json::Value>,
409}
410
411impl ToolDefinition {
412 pub fn new(name: impl Into<String>) -> Self {
414 Self {
415 name: name.into(),
416 description: None,
417 input_schema: None,
418 }
419 }
420
421 pub fn with_description(mut self, description: impl Into<String>) -> Self {
423 self.description = Some(description.into());
424 self
425 }
426
427 pub fn with_input_schema(mut self, schema: serde_json::Value) -> Self {
429 self.input_schema = Some(schema);
430 self
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use pretty_assertions::assert_eq;
438 use serde_saphyr as serde_yaml;
439 use serial_test::serial;
440
441 #[test]
446 fn test_mcp_config_deserialize() {
447 let yaml = r#"
448 command: "npx"
449 args:
450 - "-y"
451 - "@novanet/mcp-server"
452 env:
453 NEO4J_URI: "bolt://localhost:7687"
454 NEO4J_USER: "neo4j"
455 cwd: "/home/user/project"
456 "#;
457
458 let mut config: McpConfig = serde_yaml::from_str(yaml).unwrap();
459 config.name = "novanet".to_string();
460
461 assert_eq!(config.name, "novanet");
462 assert_eq!(config.command, "npx");
463 assert_eq!(config.args, vec!["-y", "@novanet/mcp-server"]);
464 assert_eq!(
465 config.env.get("NEO4J_URI"),
466 Some(&"bolt://localhost:7687".to_string())
467 );
468 assert_eq!(config.env.get("NEO4J_USER"), Some(&"neo4j".to_string()));
469 assert_eq!(config.cwd, Some("/home/user/project".to_string()));
470 }
471
472 #[test]
473 fn test_mcp_config_deserialize_minimal() {
474 let yaml = r#"
475 command: "node"
476 "#;
477
478 let config: McpConfig = serde_yaml::from_str(yaml).unwrap();
479
480 assert_eq!(config.command, "node");
481 assert!(config.args.is_empty());
482 assert!(config.env.is_empty());
483 assert!(config.cwd.is_none());
484 }
485
486 #[test]
487 fn test_mcp_config_builder() {
488 let config = McpConfig::new("test", "npx")
489 .with_args(["-y", "@test/server"])
490 .with_env("API_KEY", "secret")
491 .with_cwd("/tmp");
492
493 assert_eq!(config.name, "test");
494 assert_eq!(config.command, "npx");
495 assert_eq!(config.args, vec!["-y", "@test/server"]);
496 assert_eq!(config.env.get("API_KEY"), Some(&"secret".to_string()));
497 assert_eq!(config.cwd, Some("/tmp".to_string()));
498 }
499
500 #[test]
501 fn test_mcp_config_serialize_roundtrip() {
502 let config = McpConfig::new("test", "python")
503 .with_arg("server.py")
504 .with_env("DEBUG", "true");
505
506 let json = serde_json::to_string(&config).unwrap();
507 let parsed: McpConfig = serde_json::from_str(&json).unwrap();
508
509 assert_eq!(config.command, parsed.command);
511 assert_eq!(config.args, parsed.args);
512 assert_eq!(config.env, parsed.env);
513 }
514
515 #[test]
520 fn test_tool_call_request_new() {
521 let request = ToolCallRequest::new("novanet_context");
522
523 assert_eq!(request.name, "novanet_context");
524 assert!(request.arguments.is_object());
525 assert!(request.arguments.as_object().unwrap().is_empty());
526 }
527
528 #[test]
529 fn test_tool_call_request_with_arguments() {
530 let args = serde_json::json!({
531 "entity": "qr-code",
532 "locale": "fr-FR"
533 });
534
535 let request = ToolCallRequest::new("novanet_context").with_arguments(args.clone());
536
537 assert_eq!(request.name, "novanet_context");
538 assert_eq!(request.arguments, args);
539 }
540
541 #[test]
542 fn test_tool_call_request_deserialize() {
543 let json = r#"{
544 "name": "read_file",
545 "arguments": {
546 "path": "/tmp/test.txt"
547 }
548 }"#;
549
550 let request: ToolCallRequest = serde_json::from_str(json).unwrap();
551
552 assert_eq!(request.name, "read_file");
553 assert_eq!(request.arguments["path"], "/tmp/test.txt");
554 }
555
556 #[test]
561 fn test_tool_result_text_extraction() {
562 let result = ToolCallResult::success(vec![
563 ContentBlock::text("First line"),
564 ContentBlock::image("base64data", "image/png"),
565 ContentBlock::text("Second line"),
566 ]);
567
568 assert_eq!(result.text(), "First line\nSecond line");
569 assert_eq!(result.first_text(), Some("First line"));
570 assert!(!result.is_error);
571 }
572
573 #[test]
574 fn test_tool_result_text_extraction_empty() {
575 let result = ToolCallResult::success(vec![ContentBlock::image("data", "image/png")]);
576
577 assert_eq!(result.text(), "");
578 assert_eq!(result.first_text(), None);
579 }
580
581 #[test]
582 fn test_tool_result_error() {
583 let result = ToolCallResult::error("Something went wrong");
584
585 assert!(result.is_error);
586 assert_eq!(result.text(), "Something went wrong");
587 }
588
589 #[test]
590 fn test_tool_result_deserialize() {
591 let json = r#"{
592 "content": [
593 {"type": "text", "text": "Hello, world!"}
594 ],
595 "is_error": false
596 }"#;
597
598 let result: ToolCallResult = serde_json::from_str(json).unwrap();
599
600 assert!(!result.is_error);
601 assert_eq!(result.content.len(), 1);
602 assert_eq!(result.first_text(), Some("Hello, world!"));
603 }
604
605 #[test]
610 fn test_content_block_text() {
611 let block = ContentBlock::text("Hello");
612
613 assert!(block.is_text());
614 assert!(!block.is_image());
615 assert!(!block.is_resource());
616 assert!(matches!(block, ContentBlock::Text { ref text } if text == "Hello"));
617 }
618
619 #[test]
620 fn test_content_block_image() {
621 let block = ContentBlock::image("SGVsbG8=", "image/png");
622
623 assert!(block.is_image());
624 assert!(!block.is_text());
625 assert!(matches!(
626 block,
627 ContentBlock::Image { ref data, ref mime_type }
628 if data == "SGVsbG8=" && mime_type == "image/png"
629 ));
630 }
631
632 #[test]
633 fn test_content_block_resource() {
634 let resource = ResourceContent::new("file:///tmp/test.txt").with_text("File content");
635 let block = ContentBlock::resource(resource);
636
637 assert!(block.is_resource());
638 assert!(!block.is_text());
639 assert!(
640 matches!(block, ContentBlock::Resource(ref rc) if rc.uri == "file:///tmp/test.txt")
641 );
642 }
643
644 #[test]
645 fn test_content_block_deserialize() {
646 let json = r#"{
647 "type": "text",
648 "text": "Hello from MCP"
649 }"#;
650
651 let block: ContentBlock = serde_json::from_str(json).unwrap();
652
653 assert!(block.is_text());
654 assert!(matches!(block, ContentBlock::Text { ref text } if text == "Hello from MCP"));
655 }
656
657 #[test]
662 fn test_resource_content_builder() {
663 let resource = ResourceContent::new("neo4j://entity/qr-code")
664 .with_mime_type("application/json")
665 .with_text(r#"{"name": "QR Code"}"#);
666
667 assert_eq!(resource.uri, "neo4j://entity/qr-code");
668 assert_eq!(resource.mime_type, Some("application/json".to_string()));
669 assert_eq!(resource.text, Some(r#"{"name": "QR Code"}"#.to_string()));
670 }
671
672 #[test]
673 fn test_resource_content_deserialize() {
674 let json = r#"{
675 "uri": "file:///tmp/data.json",
676 "mimeType": "application/json",
677 "text": "{\"key\": \"value\"}"
678 }"#;
679
680 let resource: ResourceContent = serde_json::from_str(json).unwrap();
681
682 assert_eq!(resource.uri, "file:///tmp/data.json");
683 assert_eq!(resource.mime_type, Some("application/json".to_string()));
684 }
685
686 #[test]
691 fn test_tool_definition_builder() {
692 let schema = serde_json::json!({
693 "type": "object",
694 "properties": {
695 "entity": {"type": "string"},
696 "locale": {"type": "string"}
697 },
698 "required": ["entity"]
699 });
700
701 let tool = ToolDefinition::new("novanet_context")
702 .with_description("Generate native content for an entity")
703 .with_input_schema(schema.clone());
704
705 assert_eq!(tool.name, "novanet_context");
706 assert_eq!(
707 tool.description,
708 Some("Generate native content for an entity".to_string())
709 );
710 assert_eq!(tool.input_schema, Some(schema));
711 }
712
713 #[test]
714 fn test_tool_definition_deserialize() {
715 let json = r#"{
716 "name": "read_resource",
717 "description": "Read a resource from the server",
718 "inputSchema": {
719 "type": "object",
720 "properties": {
721 "uri": {"type": "string"}
722 }
723 }
724 }"#;
725
726 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
727
728 assert_eq!(tool.name, "read_resource");
729 assert_eq!(
730 tool.description,
731 Some("Read a resource from the server".to_string())
732 );
733 assert!(tool.input_schema.is_some());
734 }
735
736 #[test]
737 fn test_tool_definition_minimal() {
738 let json = r#"{"name": "ping"}"#;
739
740 let tool: ToolDefinition = serde_json::from_str(json).unwrap();
741
742 assert_eq!(tool.name, "ping");
743 assert!(tool.description.is_none());
744 assert!(tool.input_schema.is_none());
745 }
746
747 #[test]
752 fn test_mcp_error_code_standard_codes() {
753 assert_eq!(McpErrorCode::from_code(-32700), McpErrorCode::ParseError);
754 assert_eq!(
755 McpErrorCode::from_code(-32600),
756 McpErrorCode::InvalidRequest
757 );
758 assert_eq!(
759 McpErrorCode::from_code(-32601),
760 McpErrorCode::MethodNotFound
761 );
762 assert_eq!(McpErrorCode::from_code(-32602), McpErrorCode::InvalidParams);
763 assert_eq!(McpErrorCode::from_code(-32603), McpErrorCode::InternalError);
764 }
765
766 #[test]
767 fn test_mcp_error_code_server_error_range() {
768 let code = McpErrorCode::from_code(-32050);
769 assert!(matches!(code, McpErrorCode::ServerError(-32050)));
770 assert!(code.is_server_error());
771 assert!(!code.is_client_error());
772 }
773
774 #[test]
775 fn test_mcp_error_code_unknown() {
776 let code = McpErrorCode::from_code(42);
777 assert!(matches!(code, McpErrorCode::Unknown(42)));
778 assert!(!code.is_server_error());
779 assert!(!code.is_client_error());
780 }
781
782 #[test]
783 fn test_mcp_error_code_client_errors() {
784 assert!(McpErrorCode::ParseError.is_client_error());
785 assert!(McpErrorCode::InvalidRequest.is_client_error());
786 assert!(McpErrorCode::InvalidParams.is_client_error());
787 assert!(!McpErrorCode::MethodNotFound.is_client_error());
788 assert!(!McpErrorCode::InternalError.is_client_error());
789 }
790
791 #[test]
792 fn test_mcp_error_code_server_errors() {
793 assert!(McpErrorCode::MethodNotFound.is_server_error());
794 assert!(McpErrorCode::InternalError.is_server_error());
795 assert!(McpErrorCode::ServerError(-32050).is_server_error());
796 assert!(!McpErrorCode::ParseError.is_server_error());
797 }
798
799 #[test]
800 fn test_mcp_error_code_display() {
801 let code = McpErrorCode::InvalidParams;
802 let display = format!("{}", code);
803 assert!(display.contains("-32602"));
804 assert!(display.contains("Invalid method parameter"));
805 }
806
807 #[test]
808 fn test_mcp_error_code_serde_roundtrip() {
809 let original = McpErrorCode::InvalidParams;
810 let json = serde_json::to_string(&original).unwrap();
811 assert_eq!(json, "-32602");
812
813 let parsed: McpErrorCode = serde_json::from_str(&json).unwrap();
814 assert_eq!(parsed, original);
815 }
816
817 #[test]
818 fn test_mcp_error_code_into_i32() {
819 let code = McpErrorCode::ParseError;
820 let num: i32 = code.into();
821 assert_eq!(num, -32700);
822 }
823
824 #[test]
829 fn test_mcp_error_code_retryable_internal_error() {
830 let code = McpErrorCode::InternalError;
831 assert!(code.is_retryable());
832 }
833
834 #[test]
835 fn test_mcp_error_code_retryable_server_error() {
836 let code = McpErrorCode::ServerError(-32050);
837 assert!(code.is_retryable());
838 }
839
840 #[test]
841 fn test_mcp_error_code_retryable_unknown() {
842 let code = McpErrorCode::Unknown(-999);
843 assert!(code.is_retryable());
844 }
845
846 #[test]
847 fn test_mcp_error_code_not_retryable_parse_error() {
848 let code = McpErrorCode::ParseError;
849 assert!(!code.is_retryable());
850 }
851
852 #[test]
853 fn test_mcp_error_code_not_retryable_invalid_request() {
854 let code = McpErrorCode::InvalidRequest;
855 assert!(!code.is_retryable());
856 }
857
858 #[test]
859 fn test_mcp_error_code_not_retryable_invalid_params() {
860 let code = McpErrorCode::InvalidParams;
861 assert!(!code.is_retryable());
862 }
863
864 #[test]
865 fn test_mcp_error_code_not_retryable_method_not_found() {
866 let code = McpErrorCode::MethodNotFound;
867 assert!(!code.is_retryable());
868 }
869
870 #[test]
875 #[serial]
876 fn test_expand_env_vars_command() {
877 std::env::set_var("NIKA_TEST_BIN", "/usr/local/bin");
878 let config = McpConfig::new("test", "$NIKA_TEST_BIN/server")
879 .expand_env_vars()
880 .unwrap();
881
882 assert_eq!(config.command, "/usr/local/bin/server");
883 std::env::remove_var("NIKA_TEST_BIN");
884 }
885
886 #[test]
887 #[serial]
888 fn test_expand_env_vars_args() {
889 std::env::set_var("NIKA_TEST_CONFIG", "/etc/mcp");
890 let config = McpConfig::new("test", "server")
891 .with_arg("--config=$NIKA_TEST_CONFIG/config.json")
892 .expand_env_vars()
893 .unwrap();
894
895 assert_eq!(config.args[0], "--config=/etc/mcp/config.json");
896 std::env::remove_var("NIKA_TEST_CONFIG");
897 }
898
899 #[test]
900 #[serial]
901 fn test_expand_env_vars_env_values() {
902 std::env::set_var("NIKA_TEST_ROOT", "/var/lib");
903 let config = McpConfig::new("test", "server")
904 .with_env("DATA_DIR", "$NIKA_TEST_ROOT/mcp")
905 .expand_env_vars()
906 .unwrap();
907
908 assert_eq!(config.env.get("DATA_DIR").unwrap(), "/var/lib/mcp");
909 std::env::remove_var("NIKA_TEST_ROOT");
910 }
911
912 #[test]
913 fn test_expand_env_vars_tilde() {
914 let config = McpConfig::new("test", "~/bin/server")
916 .expand_env_vars()
917 .unwrap();
918
919 assert!(!config.command.contains('~'));
921 assert!(config.command.contains("/bin/server"));
922 assert!(config.command.starts_with('/'));
924 }
925
926 #[test]
927 #[serial]
928 fn test_expand_env_vars_curly_brace_syntax() {
929 std::env::set_var("NIKA_TEST_PATH", "/opt/mcp");
930 let config = McpConfig::new("test", "${NIKA_TEST_PATH}/server")
931 .expand_env_vars()
932 .unwrap();
933
934 assert_eq!(config.command, "/opt/mcp/server");
935 std::env::remove_var("NIKA_TEST_PATH");
936 }
937
938 #[test]
939 fn test_expand_env_vars_no_expansion_needed() {
940 let config = McpConfig::new("test", "/usr/bin/server")
941 .with_arg("--port=8080")
942 .with_env("LOG_LEVEL", "debug")
943 .expand_env_vars()
944 .unwrap();
945
946 assert_eq!(config.command, "/usr/bin/server");
947 assert_eq!(config.args[0], "--port=8080");
948 assert_eq!(config.env.get("LOG_LEVEL").unwrap(), "debug");
949 }
950
951 #[test]
952 #[serial]
953 fn test_expand_env_vars_cwd() {
954 std::env::set_var("NIKA_TEST_DIR", "/home/user/projects");
955 let config = McpConfig::new("test", "server")
956 .with_cwd("$NIKA_TEST_DIR/mcp")
957 .expand_env_vars()
958 .unwrap();
959
960 assert_eq!(config.cwd.unwrap(), "/home/user/projects/mcp");
961 std::env::remove_var("NIKA_TEST_DIR");
962 }
963
964 #[test]
969 fn test_content_block_audio_enum() {
970 let block = ContentBlock::audio("b64audio", "audio/wav");
971 assert!(block.is_audio());
972 assert!(!block.is_text());
973 assert!(!block.is_image());
974 assert!(matches!(
975 block,
976 ContentBlock::Audio { ref data, ref mime_type }
977 if data == "b64audio" && mime_type == "audio/wav"
978 ));
979 }
980
981 #[test]
982 fn test_content_block_resource_link_enum() {
983 let block = ContentBlock::resource_link(
984 "file:///tmp/test.txt",
985 Some("test.txt".to_string()),
986 Some("text/plain".to_string()),
987 );
988 assert!(block.is_resource_link());
989 assert!(!block.is_text());
990 assert!(!block.is_resource());
991 }
992
993 #[test]
994 fn test_content_block_serde_roundtrip_all_variants() {
995 let blocks = [
996 ContentBlock::text("hello"),
997 ContentBlock::image("b64", "image/png"),
998 ContentBlock::audio("b64", "audio/wav"),
999 ContentBlock::resource(ResourceContent::new("file:///test").with_text("content")),
1000 ContentBlock::resource_link("file:///link", None, None),
1001 ];
1002 for (i, block) in blocks.iter().enumerate() {
1003 let json = serde_json::to_string(block)
1004 .unwrap_or_else(|e| panic!("variant {i} failed to serialize: {e}"));
1005 let back: ContentBlock = serde_json::from_str(&json)
1006 .unwrap_or_else(|e| panic!("variant {i} failed to deserialize: {e}\nJSON: {json}"));
1007 assert_eq!(*block, back, "variant {i} roundtrip mismatch");
1008 }
1009 }
1010
1011 #[test]
1012 fn test_debug_print_all_variants_json() {
1013 let blocks = vec![
1014 ("text", ContentBlock::text("hello")),
1015 ("image", ContentBlock::image("b64", "image/png")),
1016 ("audio", ContentBlock::audio("b64", "audio/wav")),
1017 (
1018 "resource",
1019 ContentBlock::resource(ResourceContent::new("file:///test").with_text("content")),
1020 ),
1021 (
1022 "resource_link_none",
1023 ContentBlock::resource_link("file:///link", None, None),
1024 ),
1025 (
1026 "resource_link_full",
1027 ContentBlock::resource_link(
1028 "file:///link",
1029 Some("test.txt".into()),
1030 Some("text/plain".into()),
1031 ),
1032 ),
1033 ];
1034 for (label, block) in &blocks {
1035 let json = serde_json::to_string_pretty(block).unwrap();
1036 println!("=== {label} ===\n{json}\n");
1037 }
1038 }
1039
1040 #[test]
1041 fn test_content_block_text_json_format() {
1042 let block = ContentBlock::text("hello world");
1043 let json = serde_json::to_value(&block).unwrap();
1044 assert_eq!(json["type"], "text");
1045 assert_eq!(json["text"], "hello world");
1046 }
1047
1048 #[test]
1049 fn test_content_block_image_json_has_mime_type_camel_case() {
1050 let block = ContentBlock::image("b64data", "image/png");
1051 let json = serde_json::to_value(&block).unwrap();
1052 assert_eq!(json["type"], "image");
1053 assert_eq!(json["mimeType"], "image/png");
1054 assert!(
1055 json.get("mime_type").is_none(),
1056 "should use mimeType not mime_type"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_content_block_resource_link_skips_none_fields() {
1062 let block = ContentBlock::resource_link("file:///link", None, None);
1063 let json = serde_json::to_string(&block).unwrap();
1064 assert!(!json.contains("name"), "None name should be skipped");
1065 assert!(
1066 !json.contains("mimeType"),
1067 "None mimeType should be skipped"
1068 );
1069 }
1070
1071 #[test]
1076 fn test_has_media_false_for_text_only() {
1077 let result = ToolCallResult::success(vec![ContentBlock::text("hello")]);
1078 assert!(!result.has_media());
1079 assert!(result.images().is_empty());
1080 assert!(result.audio_blocks().is_empty());
1081 assert!(result.media_blocks().is_empty());
1082 }
1083
1084 #[test]
1085 fn test_has_media_true_for_mixed_content() {
1086 let result = ToolCallResult::success(vec![
1087 ContentBlock::text("description"),
1088 ContentBlock::image("b64", "image/png"),
1089 ContentBlock::audio("b64", "audio/wav"),
1090 ]);
1091 assert!(result.has_media());
1092 assert_eq!(result.images().len(), 1);
1093 assert_eq!(result.audio_blocks().len(), 1);
1094 assert_eq!(result.media_blocks().len(), 2);
1095 }
1096
1097 #[test]
1098 fn test_text_preserved_with_media() {
1099 let result = ToolCallResult::success(vec![
1100 ContentBlock::text("First line"),
1101 ContentBlock::image("b64data", "image/png"),
1102 ContentBlock::text("Second line"),
1103 ]);
1104 assert_eq!(result.text(), "First line\nSecond line");
1105 assert_eq!(result.first_text(), Some("First line"));
1106 assert!(result.has_media());
1107 }
1108
1109 #[test]
1110 fn test_empty_content_vec() {
1111 let result = ToolCallResult::success(vec![]);
1112 assert!(!result.has_media());
1113 assert_eq!(result.text(), "");
1114 assert!(result.images().is_empty());
1115 assert!(result.media_blocks().is_empty());
1116 }
1117
1118 #[test]
1119 fn test_with_blob_builder() {
1120 let rc = ResourceContent::new("file:///test")
1121 .with_blob("base64data")
1122 .with_mime_type("application/pdf");
1123 assert_eq!(rc.blob, Some("base64data".to_string()));
1124 assert_eq!(rc.mime_type, Some("application/pdf".to_string()));
1125 }
1126
1127 #[test]
1140 fn test_deser_text_from_mcp_json() {
1141 let json = r#"{"type": "text", "text": "hello"}"#;
1142 let block: ContentBlock = serde_json::from_str(json).unwrap();
1143 assert!(block.is_text());
1144 assert_eq!(
1145 block,
1146 ContentBlock::Text {
1147 text: "hello".into()
1148 }
1149 );
1150 }
1151
1152 #[test]
1153 fn test_deser_image_from_mcp_json() {
1154 let json = r#"{"type": "image", "data": "b64data", "mimeType": "image/png"}"#;
1155 let block: ContentBlock = serde_json::from_str(json).unwrap();
1156 assert!(block.is_image());
1157 assert_eq!(
1158 block,
1159 ContentBlock::Image {
1160 data: "b64data".into(),
1161 mime_type: "image/png".into(),
1162 }
1163 );
1164 }
1165
1166 #[test]
1167 fn test_deser_audio_from_mcp_json() {
1168 let json = r#"{"type": "audio", "data": "b64data", "mimeType": "audio/wav"}"#;
1169 let block: ContentBlock = serde_json::from_str(json).unwrap();
1170 assert!(block.is_audio());
1171 assert_eq!(
1172 block,
1173 ContentBlock::Audio {
1174 data: "b64data".into(),
1175 mime_type: "audio/wav".into(),
1176 }
1177 );
1178 }
1179
1180 #[test]
1181 fn test_deser_resource_from_mcp_json() {
1182 let json = r#"{"type": "resource", "uri": "file:///test", "text": "content"}"#;
1183 let block: ContentBlock = serde_json::from_str(json).unwrap();
1184 assert!(block.is_resource());
1185 let expected =
1186 ContentBlock::Resource(ResourceContent::new("file:///test").with_text("content"));
1187 assert_eq!(block, expected);
1188 }
1189
1190 #[test]
1191 fn test_deser_resource_link_from_mcp_json() {
1192 let json = r#"{"type": "resource_link", "uri": "file:///link"}"#;
1193 let block: ContentBlock = serde_json::from_str(json).unwrap();
1194 assert!(block.is_resource_link());
1195 assert_eq!(
1196 block,
1197 ContentBlock::ResourceLink {
1198 uri: "file:///link".into(),
1199 name: None,
1200 mime_type: None,
1201 }
1202 );
1203 }
1204
1205 #[test]
1206 fn test_deser_resource_link_with_optional_fields() {
1207 let json = r#"{
1208 "type": "resource_link",
1209 "uri": "file:///link",
1210 "name": "report.pdf",
1211 "mimeType": "application/pdf"
1212 }"#;
1213 let block: ContentBlock = serde_json::from_str(json).unwrap();
1214 assert_eq!(
1215 block,
1216 ContentBlock::ResourceLink {
1217 uri: "file:///link".into(),
1218 name: Some("report.pdf".into()),
1219 mime_type: Some("application/pdf".into()),
1220 }
1221 );
1222 }
1223
1224 #[test]
1229 fn test_deser_image_missing_mime_type_fails() {
1230 let json = r#"{"type": "image", "data": "x"}"#;
1231 let result = serde_json::from_str::<ContentBlock>(json);
1232 assert!(
1233 result.is_err(),
1234 "image without mimeType should fail to deserialize"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_deser_image_missing_data_fails() {
1240 let json = r#"{"type": "image", "mimeType": "image/png"}"#;
1241 let result = serde_json::from_str::<ContentBlock>(json);
1242 assert!(
1243 result.is_err(),
1244 "image without data should fail to deserialize"
1245 );
1246 }
1247
1248 #[test]
1249 fn test_deser_audio_missing_mime_type_fails() {
1250 let json = r#"{"type": "audio", "data": "x"}"#;
1251 let result = serde_json::from_str::<ContentBlock>(json);
1252 assert!(
1253 result.is_err(),
1254 "audio without mimeType should fail to deserialize"
1255 );
1256 }
1257
1258 #[test]
1259 fn test_deser_audio_missing_data_fails() {
1260 let json = r#"{"type": "audio", "mimeType": "audio/wav"}"#;
1261 let result = serde_json::from_str::<ContentBlock>(json);
1262 assert!(
1263 result.is_err(),
1264 "audio without data should fail to deserialize"
1265 );
1266 }
1267
1268 #[test]
1269 fn test_deser_text_missing_text_field_fails() {
1270 let json = r#"{"type": "text"}"#;
1271 let result = serde_json::from_str::<ContentBlock>(json);
1272 assert!(
1273 result.is_err(),
1274 "text without text field should fail to deserialize"
1275 );
1276 }
1277
1278 #[test]
1279 fn test_deser_resource_missing_uri_fails() {
1280 let json = r#"{"type": "resource", "text": "content"}"#;
1281 let result = serde_json::from_str::<ContentBlock>(json);
1282 assert!(
1283 result.is_err(),
1284 "resource without uri should fail to deserialize"
1285 );
1286 }
1287
1288 #[test]
1289 fn test_deser_resource_link_missing_uri_fails() {
1290 let json = r#"{"type": "resource_link", "name": "test.txt"}"#;
1291 let result = serde_json::from_str::<ContentBlock>(json);
1292 assert!(
1293 result.is_err(),
1294 "resource_link without uri should fail to deserialize"
1295 );
1296 }
1297
1298 #[test]
1299 fn test_deser_extra_unknown_fields_succeeds() {
1300 let json = r#"{"type": "text", "text": "hi", "extra": true, "count": 42}"#;
1302 let block: ContentBlock = serde_json::from_str(json).unwrap();
1303 assert_eq!(block, ContentBlock::text("hi"));
1304 }
1305
1306 #[test]
1307 fn test_deser_invalid_type_value_fails() {
1308 let json = r#"{"type": "video", "data": "x"}"#;
1309 let result = serde_json::from_str::<ContentBlock>(json);
1310 assert!(result.is_err(), "unknown type 'video' should fail");
1311 }
1312
1313 #[test]
1314 fn test_deser_empty_string_type_fails() {
1315 let json = r#"{"type": "", "text": "hello"}"#;
1316 let result = serde_json::from_str::<ContentBlock>(json);
1317 assert!(result.is_err(), "empty string type should fail");
1318 }
1319
1320 #[test]
1321 fn test_deser_missing_type_field_fails() {
1322 let json = r#"{"text": "hello"}"#;
1323 let result = serde_json::from_str::<ContentBlock>(json);
1324 assert!(result.is_err(), "missing type field should fail");
1325 }
1326
1327 #[test]
1328 fn test_deser_null_type_fails() {
1329 let json = r#"{"type": null, "text": "hello"}"#;
1330 let result = serde_json::from_str::<ContentBlock>(json);
1331 assert!(result.is_err(), "null type should fail");
1332 }
1333
1334 #[test]
1335 fn test_deser_numeric_type_fails() {
1336 let json = r#"{"type": 42, "text": "hello"}"#;
1337 let result = serde_json::from_str::<ContentBlock>(json);
1338 assert!(result.is_err(), "numeric type should fail");
1339 }
1340
1341 #[test]
1346 fn test_resource_serializes_flat_not_nested() {
1347 let block = ContentBlock::resource(
1348 ResourceContent::new("file:///test")
1349 .with_text("hello")
1350 .with_mime_type("text/plain"),
1351 );
1352 let json = serde_json::to_value(&block).unwrap();
1353
1354 assert_eq!(json["type"], "resource");
1356 assert_eq!(json["uri"], "file:///test");
1357 assert_eq!(json["text"], "hello");
1358 assert_eq!(json["mimeType"], "text/plain");
1359 assert!(
1360 json.get("resource").is_none(),
1361 "should NOT have nested 'resource' key"
1362 );
1363 }
1364
1365 #[test]
1366 fn test_resource_content_none_fields_omitted_in_serialization() {
1367 let rc = ResourceContent::new("file:///bare");
1368 let json = serde_json::to_value(&rc).unwrap();
1369
1370 assert_eq!(json["uri"], "file:///bare");
1371 assert!(
1372 json.get("mimeType").is_none(),
1373 "None mimeType should be omitted"
1374 );
1375 assert!(json.get("text").is_none(), "None text should be omitted");
1376 assert!(json.get("blob").is_none(), "None blob should be omitted");
1377 }
1378
1379 #[test]
1380 fn test_resource_content_some_fields_present_in_serialization() {
1381 let rc = ResourceContent::new("file:///full")
1382 .with_mime_type("application/json")
1383 .with_text("{}")
1384 .with_blob("YmluYXJ5");
1385 let json = serde_json::to_value(&rc).unwrap();
1386
1387 assert_eq!(json["uri"], "file:///full");
1388 assert_eq!(json["mimeType"], "application/json");
1389 assert_eq!(json["text"], "{}");
1390 assert_eq!(json["blob"], "YmluYXJ5");
1391 }
1392
1393 #[test]
1394 fn test_resource_block_none_fields_omitted_via_content_block() {
1395 let block = ContentBlock::resource(ResourceContent::new("file:///bare"));
1397 let json_str = serde_json::to_string(&block).unwrap();
1398
1399 assert!(
1400 !json_str.contains("mimeType"),
1401 "None mimeType should be omitted"
1402 );
1403 assert!(
1404 !json_str.contains("\"text\""),
1405 "None text should be omitted"
1406 );
1407 assert!(!json_str.contains("blob"), "None blob should be omitted");
1408 assert!(json_str.contains("\"type\""));
1409 assert!(json_str.contains("\"uri\""));
1410 }
1411
1412 #[test]
1413 fn test_resource_content_roundtrip_with_all_fields() {
1414 let original = ResourceContent::new("file:///test")
1415 .with_mime_type("application/octet-stream")
1416 .with_text("textual fallback")
1417 .with_blob("YmxvYg==");
1418
1419 let json = serde_json::to_string(&original).unwrap();
1420 let parsed: ResourceContent = serde_json::from_str(&json).unwrap();
1421 assert_eq!(original, parsed);
1422 }
1423
1424 #[test]
1425 fn test_resource_content_roundtrip_with_no_optional_fields() {
1426 let original = ResourceContent::new("file:///minimal");
1427 let json = serde_json::to_string(&original).unwrap();
1428 let parsed: ResourceContent = serde_json::from_str(&json).unwrap();
1429 assert_eq!(original, parsed);
1430 }
1431
1432 #[test]
1437 fn test_tool_call_result_deser_mixed_content() {
1438 let json = r#"{
1439 "content": [
1440 {"type": "text", "text": "Analysis complete"},
1441 {"type": "image", "data": "iVBORw0KGgo=", "mimeType": "image/png"},
1442 {"type": "audio", "data": "UklGRg==", "mimeType": "audio/wav"},
1443 {"type": "text", "text": "See attached media"},
1444 {"type": "resource", "uri": "file:///data.json", "text": "{\"key\": 1}"},
1445 {"type": "resource_link", "uri": "file:///extra"}
1446 ],
1447 "is_error": false
1448 }"#;
1449
1450 let result: ToolCallResult = serde_json::from_str(json).unwrap();
1451
1452 assert!(!result.is_error);
1453 assert_eq!(result.content.len(), 6);
1454
1455 assert_eq!(result.text(), "Analysis complete\nSee attached media");
1457
1458 assert!(result.has_media());
1460
1461 assert_eq!(result.media_blocks().len(), 4);
1463
1464 assert_eq!(result.images().len(), 1);
1466 assert!(result.images()[0].is_image());
1467
1468 assert_eq!(result.audio_blocks().len(), 1);
1470 assert!(result.audio_blocks()[0].is_audio());
1471
1472 assert_eq!(result.first_text(), Some("Analysis complete"));
1474 }
1475
1476 #[test]
1477 fn test_tool_call_result_deser_error_with_mixed_content() {
1478 let json = r#"{
1479 "content": [
1480 {"type": "text", "text": "Partial failure"},
1481 {"type": "image", "data": "corrupt", "mimeType": "image/jpeg"}
1482 ],
1483 "is_error": true
1484 }"#;
1485
1486 let result: ToolCallResult = serde_json::from_str(json).unwrap();
1487 assert!(result.is_error);
1488 assert!(result.has_media());
1489 assert_eq!(result.text(), "Partial failure");
1490 }
1491
1492 #[test]
1493 fn test_tool_call_result_deser_is_error_defaults_false() {
1494 let json = r#"{
1496 "content": [{"type": "text", "text": "ok"}]
1497 }"#;
1498
1499 let result: ToolCallResult = serde_json::from_str(json).unwrap();
1500 assert!(!result.is_error);
1501 }
1502
1503 #[test]
1504 fn test_tool_call_result_roundtrip_mixed() {
1505 let original = ToolCallResult::success(vec![
1506 ContentBlock::text("output"),
1507 ContentBlock::image("aW1n", "image/webp"),
1508 ContentBlock::audio("YXVk", "audio/mp3"),
1509 ContentBlock::resource(ResourceContent::new("file:///r").with_text("resource text")),
1510 ContentBlock::resource_link("file:///rl", Some("name".into()), None),
1511 ]);
1512
1513 let json = serde_json::to_string(&original).unwrap();
1514 let parsed: ToolCallResult = serde_json::from_str(&json).unwrap();
1515 assert_eq!(original, parsed);
1516 }
1517
1518 #[test]
1519 fn test_tool_call_result_empty_content_array() {
1520 let json = r#"{"content": [], "is_error": false}"#;
1521 let result: ToolCallResult = serde_json::from_str(json).unwrap();
1522 assert_eq!(result.content.len(), 0);
1523 assert!(!result.has_media());
1524 assert_eq!(result.text(), "");
1525 assert!(result.first_text().is_none());
1526 }
1527
1528 #[test]
1533 fn test_text_block_with_emoji() {
1534 let json = r#"{"type": "text", "text": "Hello 🌍"}"#;
1535 let block: ContentBlock = serde_json::from_str(json).unwrap();
1536 assert_eq!(block, ContentBlock::text("Hello 🌍"));
1537 }
1538
1539 #[test]
1540 fn test_text_block_with_newlines() {
1541 let json = "{\"type\": \"text\", \"text\": \"line1\\nline2\"}";
1542 let block: ContentBlock = serde_json::from_str(json).unwrap();
1543 assert_eq!(block, ContentBlock::text("line1\nline2"));
1544 }
1545
1546 #[test]
1547 fn test_text_block_with_unicode_escapes() {
1548 let json = r#"{"type": "text", "text": "caf\u00e9"}"#;
1550 let block: ContentBlock = serde_json::from_str(json).unwrap();
1551 assert_eq!(block, ContentBlock::text("caf\u{00e9}"));
1552 }
1553
1554 #[test]
1555 fn test_text_block_with_json_special_chars() {
1556 let json = r#"{"type": "text", "text": "quote: \" backslash: \\ tab: \t"}"#;
1558 let block: ContentBlock = serde_json::from_str(json).unwrap();
1559 assert_eq!(block, ContentBlock::text("quote: \" backslash: \\ tab: \t"));
1560 }
1561
1562 #[test]
1563 fn test_text_block_empty_string() {
1564 let json = r#"{"type": "text", "text": ""}"#;
1565 let block: ContentBlock = serde_json::from_str(json).unwrap();
1566 assert_eq!(block, ContentBlock::text(""));
1567 }
1568
1569 #[test]
1570 fn test_image_data_with_base64_special_chars() {
1571 let b64 = "abc+def/ghi=";
1573 let json = format!(
1574 r#"{{"type": "image", "data": "{}", "mimeType": "image/png"}}"#,
1575 b64
1576 );
1577 let block: ContentBlock = serde_json::from_str(&json).unwrap();
1578 assert_eq!(
1579 block,
1580 ContentBlock::Image {
1581 data: b64.into(),
1582 mime_type: "image/png".into(),
1583 }
1584 );
1585 }
1586
1587 #[test]
1588 fn test_image_data_with_double_padding() {
1589 let b64 = "YQ==";
1590 let json = format!(
1591 r#"{{"type": "image", "data": "{}", "mimeType": "image/gif"}}"#,
1592 b64
1593 );
1594 let block: ContentBlock = serde_json::from_str(&json).unwrap();
1595 assert_eq!(
1596 block,
1597 ContentBlock::Image {
1598 data: b64.into(),
1599 mime_type: "image/gif".into(),
1600 }
1601 );
1602 }
1603
1604 #[test]
1605 fn test_resource_uri_with_special_chars() {
1606 let json = r#"{"type": "resource", "uri": "file:///path/to/my%20file.txt", "text": "ok"}"#;
1607 let block: ContentBlock = serde_json::from_str(json).unwrap();
1608 assert!(block.is_resource());
1609 if let ContentBlock::Resource(rc) = &block {
1610 assert_eq!(rc.uri, "file:///path/to/my%20file.txt");
1611 }
1612 }
1613
1614 #[test]
1619 fn test_image_serialization_uses_camel_case_mime_type() {
1620 let block = ContentBlock::image("data", "image/jpeg");
1621 let json = serde_json::to_value(&block).unwrap();
1622
1623 assert!(
1625 json.get("mimeType").is_some(),
1626 "should serialize as mimeType"
1627 );
1628 assert!(
1629 json.get("mime_type").is_none(),
1630 "should NOT serialize as mime_type"
1631 );
1632 }
1633
1634 #[test]
1635 fn test_audio_serialization_uses_camel_case_mime_type() {
1636 let block = ContentBlock::audio("data", "audio/ogg");
1637 let json = serde_json::to_value(&block).unwrap();
1638
1639 assert!(
1640 json.get("mimeType").is_some(),
1641 "should serialize as mimeType"
1642 );
1643 assert!(
1644 json.get("mime_type").is_none(),
1645 "should NOT serialize as mime_type"
1646 );
1647 }
1648
1649 #[test]
1650 fn test_resource_link_serialization_uses_camel_case_mime_type() {
1651 let block = ContentBlock::resource_link("file:///x", None, Some("text/html".into()));
1652 let json = serde_json::to_value(&block).unwrap();
1653
1654 assert!(
1655 json.get("mimeType").is_some(),
1656 "should serialize as mimeType"
1657 );
1658 assert!(
1659 json.get("mime_type").is_none(),
1660 "should NOT serialize as mime_type"
1661 );
1662 }
1663
1664 #[test]
1665 fn test_resource_content_serialization_uses_camel_case_mime_type() {
1666 let rc = ResourceContent::new("file:///x").with_mime_type("text/plain");
1667 let json = serde_json::to_value(&rc).unwrap();
1668
1669 assert!(
1670 json.get("mimeType").is_some(),
1671 "should serialize as mimeType"
1672 );
1673 assert!(
1674 json.get("mime_type").is_none(),
1675 "should NOT serialize as mime_type"
1676 );
1677 }
1678
1679 #[test]
1680 fn test_image_deser_rejects_snake_case_mime_type() {
1681 let json = r#"{"type": "image", "data": "x", "mime_type": "image/png"}"#;
1683 let result = serde_json::from_str::<ContentBlock>(json);
1684 assert!(
1685 result.is_err(),
1686 "snake_case mime_type should fail — serde renames to mimeType"
1687 );
1688 }
1689
1690 #[test]
1691 fn test_audio_deser_rejects_snake_case_mime_type() {
1692 let json = r#"{"type": "audio", "data": "x", "mime_type": "audio/wav"}"#;
1693 let result = serde_json::from_str::<ContentBlock>(json);
1694 assert!(
1695 result.is_err(),
1696 "snake_case mime_type should fail — serde renames to mimeType"
1697 );
1698 }
1699
1700 #[test]
1705 fn test_type_tag_values_are_snake_case() {
1706 let cases: Vec<(ContentBlock, &str)> = vec![
1707 (ContentBlock::text("t"), "text"),
1708 (ContentBlock::image("d", "image/png"), "image"),
1709 (ContentBlock::audio("d", "audio/wav"), "audio"),
1710 (
1711 ContentBlock::resource(ResourceContent::new("file:///x")),
1712 "resource",
1713 ),
1714 (
1715 ContentBlock::resource_link("file:///x", None, None),
1716 "resource_link",
1717 ),
1718 ];
1719
1720 for (block, expected_tag) in cases {
1721 let json = serde_json::to_value(&block).unwrap();
1722 assert_eq!(
1723 json["type"].as_str().unwrap(),
1724 expected_tag,
1725 "wrong type tag for {:?}",
1726 block
1727 );
1728 }
1729 }
1730
1731 #[test]
1732 fn test_camel_case_type_tag_fails() {
1733 let json = r#"{"type": "resourceLink", "uri": "file:///x"}"#;
1735 let result = serde_json::from_str::<ContentBlock>(json);
1736 assert!(result.is_err(), "camelCase type tag should fail");
1737 }
1738
1739 #[test]
1744 fn test_deser_resource_with_blob_no_text() {
1745 let json = r#"{"type": "resource", "uri": "file:///bin", "blob": "AQID"}"#;
1746 let block: ContentBlock = serde_json::from_str(json).unwrap();
1747 if let ContentBlock::Resource(rc) = &block {
1748 assert_eq!(rc.uri, "file:///bin");
1749 assert_eq!(rc.blob, Some("AQID".into()));
1750 assert!(rc.text.is_none());
1751 assert!(rc.mime_type.is_none());
1752 } else {
1753 panic!("expected Resource variant");
1754 }
1755 }
1756
1757 #[test]
1758 fn test_deser_resource_uri_only() {
1759 let json = r#"{"type": "resource", "uri": "file:///bare"}"#;
1760 let block: ContentBlock = serde_json::from_str(json).unwrap();
1761 let expected = ContentBlock::resource(ResourceContent::new("file:///bare"));
1762 assert_eq!(block, expected);
1763 }
1764
1765 #[test]
1766 fn test_content_block_clone_eq() {
1767 let blocks = vec![
1768 ContentBlock::text("t"),
1769 ContentBlock::image("d", "image/png"),
1770 ContentBlock::audio("d", "audio/wav"),
1771 ContentBlock::resource(ResourceContent::new("file:///x")),
1772 ContentBlock::resource_link("file:///x", Some("n".into()), Some("m".into())),
1773 ];
1774 for block in &blocks {
1775 let cloned = block.clone();
1776 assert_eq!(*block, cloned);
1777 }
1778 }
1779
1780 #[test]
1781 fn test_large_text_block_roundtrip() {
1782 let large_text = "a".repeat(100_000);
1783 let block = ContentBlock::text(&large_text);
1784 let json = serde_json::to_string(&block).unwrap();
1785 let parsed: ContentBlock = serde_json::from_str(&json).unwrap();
1786 assert_eq!(block, parsed);
1787 }
1788
1789 #[test]
1790 fn test_large_base64_data_roundtrip() {
1791 let large_data = "A".repeat(1_000_000); let block = ContentBlock::image(&large_data, "image/tiff");
1793 let json = serde_json::to_string(&block).unwrap();
1794 let parsed: ContentBlock = serde_json::from_str(&json).unwrap();
1795 assert_eq!(block, parsed);
1796 }
1797
1798 #[test]
1799 fn test_tool_call_result_many_blocks_roundtrip() {
1800 let blocks: Vec<ContentBlock> = (0..100)
1801 .map(|i| {
1802 if i % 3 == 0 {
1803 ContentBlock::text(format!("block-{i}"))
1804 } else if i % 3 == 1 {
1805 ContentBlock::image(format!("data-{i}"), "image/png")
1806 } else {
1807 ContentBlock::audio(format!("audio-{i}"), "audio/ogg")
1808 }
1809 })
1810 .collect();
1811
1812 let original = ToolCallResult::success(blocks);
1813 let json = serde_json::to_string(&original).unwrap();
1814 let parsed: ToolCallResult = serde_json::from_str(&json).unwrap();
1815 assert_eq!(original, parsed);
1816 assert_eq!(parsed.content.len(), 100);
1817 }
1818}