//! MiniMax M2 Parser Integration Tests use smg::tool_parser::{MinimaxM2Parser, ToolParser}; use crate::common::create_test_tools; #[tokio::test] async fn test_minimax_complete_parsing() { let parser = MinimaxM2Parser::new(); let input = r#"Let me search for that. Beijing 2024-12-25 The weather will be..."#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); assert_eq!(normal_text, "Let me search for that.\n"); assert_eq!(tools[0].function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["city"], "Beijing"); assert_eq!(args["date"], "2024-12-25"); } #[tokio::test] async fn test_minimax_multiple_tools() { let parser = MinimaxM2Parser::new(); let input = r#" rust tutorials Hello World zh "#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 2); assert_eq!(normal_text, ""); assert_eq!(tools[0].function.name, "search"); assert_eq!(tools[1].function.name, "translate"); } #[tokio::test] async fn test_minimax_type_conversion() { let parser = MinimaxM2Parser::new(); let input = r#" 42 1.5 true null string value "#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); assert_eq!(normal_text, ""); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["count"], 42); assert_eq!(args["rate"], 1.5); assert_eq!(args["enabled"], true); assert_eq!(args["data"], serde_json::Value::Null); assert_eq!(args["text"], "string value"); } #[tokio::test] async fn test_minimax_streaming_basic() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); // Simulate streaming chunks let chunks = vec![ "", r#""#, r#"Shanghai"#, r#"celsius"#, "", "", ]; let mut found_name = false; let mut found_params = false; for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); for call in result.calls { if let Some(name) = call.name { assert_eq!(name, "get_weather"); found_name = true; } if !call.parameters.is_empty() { found_params = true; } } } assert!(found_name, "Should have found tool name during streaming"); assert!(found_params, "Should have streamed parameters"); } #[test] fn test_minimax_format_detection() { let parser = MinimaxM2Parser::new(); // Should detect MiniMax format assert!(parser.has_tool_markers("")); assert!(parser.has_tool_markers("text with marker")); // Should not detect other formats assert!(!parser.has_tool_markers("")); // GLM4 format assert!(!parser.has_tool_markers("[TOOL_CALLS]")); assert!(!parser.has_tool_markers("<|tool▁calls▁begin|>")); assert!(!parser.has_tool_markers("plain text")); } #[tokio::test] async fn test_minimax_python_literals() { let parser = MinimaxM2Parser::new(); let input = r#" True False None "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); assert_eq!(tools[0].function.name, "test_func"); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["bool_true"], true); assert_eq!(args["bool_false"], false); assert_eq!(args["none_val"], serde_json::Value::Null); } #[tokio::test] async fn test_minimax_nested_json_in_parameters() { let parser = MinimaxM2Parser::new(); let input = r#" {"nested": {"key": "value"}} [1, 2, 3] "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); // JSON-like strings are kept as strings, not parsed as JSON // This matches the behavior of other parsers like GLM4 MOE assert!(args["data"].is_string()); assert_eq!(args["data"], r#"{"nested": {"key": "value"}}"#); assert!(args["list"].is_string()); assert_eq!(args["list"], "[1, 2, 3]"); } #[tokio::test] async fn test_minimax_xml_entities() { let parser = MinimaxM2Parser::new(); let input = r#" <div>content</div> Quote: "hello" if (a && b) { } "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["html"], "
content
"); assert_eq!(args["text"], "Quote: \"hello\""); assert_eq!(args["code"], "if (a && b) { }"); } #[tokio::test] async fn test_minimax_streaming_partial_tags() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); // Chunks split mid-tag let chunks = vec![ "Bei"#, "jing
", ]; let mut found_name = false; let mut buffer = String::new(); for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); buffer.push_str(&result.normal_text); for call in result.calls { if let Some(name) = call.name { assert_eq!(name, "get_weather"); found_name = true; } } } assert!( found_name, "Should have parsed function name from partial chunks" ); assert_eq!(buffer, ""); } #[tokio::test] async fn test_minimax_streaming_incremental_json() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); let chunks = vec![ "", r#""#, r#"Paris"#, r#"metric"#, "", ]; let mut json_fragments = Vec::new(); let mut found_function = false; for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); for call in result.calls { if let Some(_name) = call.name { found_function = true; } if !call.parameters.is_empty() { json_fragments.push(call.parameters.clone()); } } } assert!(found_function); // Verify JSON was built incrementally assert!(!json_fragments.is_empty()); // First fragment should start with opening brace if let Some(first) = json_fragments.first() { assert!( first.starts_with('{'), "First JSON fragment should start with '{{': {}", first ); } // Last fragment should be closing brace if let Some(last) = json_fragments.last() { assert!( last.contains('}'), "Last JSON fragment should contain '}}': {}", last ); } } #[tokio::test] async fn test_minimax_multiple_tools_boundary() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); // Tool boundary at chunk boundary let chunks = vec![ r#"Tokyo"#, r#"weather forecast"#, ]; let mut tool_names = Vec::new(); for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); for call in result.calls { if let Some(name) = call.name { tool_names.push(name); } } } assert_eq!(tool_names.len(), 2); assert_eq!(tool_names[0], "get_weather"); assert_eq!(tool_names[1], "search"); } #[tokio::test] async fn test_minimax_invalid_function_name() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); let chunks = vec![ "", r#""#, r#"value"#, "", ]; let mut found_invalid = false; for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); // Invalid function should be skipped for call in result.calls { if let Some(name) = call.name { if name == "invalid_function" { found_invalid = true; } } } } assert!(!found_invalid, "Invalid function should not be parsed"); } #[tokio::test] async fn test_minimax_empty_parameters() { let parser = MinimaxM2Parser::new(); let input = r#" "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); assert_eq!(tools[0].function.name, "simple_func"); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args, serde_json::json!({})); } #[tokio::test] async fn test_minimax_multiline_parameter_values() { let parser = MinimaxM2Parser::new(); let input = r#" line1 line2 line3 你好世界 🌍 "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["multiline"], "line1\nline2\nline3"); assert_eq!(args["unicode"], "你好世界 🌍"); } #[tokio::test] async fn test_minimax_nested_xml_like_content() { let parser = MinimaxM2Parser::new(); let input = r#" Hello {"key": "nested"} "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["template"], "Hello"); // The nested JSON with XML-like content let config = serde_json::from_str::(args["config"].as_str().unwrap()).unwrap(); assert_eq!(config["key"], "nested"); } #[tokio::test] async fn test_minimax_streaming_state_reset() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); // First tool let chunks1 = vec![ r#""#, r#"London"#, "", ]; for chunk in chunks1 { parser.parse_incremental(chunk, &tools).await.unwrap(); } // Second tool - state should be reset let chunks2 = vec![ r#""#, r#"rust"#, "", ]; let mut second_tool_name = None; for chunk in chunks2 { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); for call in result.calls { if let Some(name) = call.name { second_tool_name = Some(name); } } } assert_eq!(second_tool_name, Some("search".to_string())); } #[tokio::test] async fn test_minimax_many_parameters() { let parser = MinimaxM2Parser::new(); let mut params_xml = String::new(); for i in 1..=20 { params_xml.push_str(&format!( r#"value{} "#, i, i )); } let input = format!( r#" {} "#, params_xml ); let (_normal_text, tools) = parser.parse_complete(&input).await.unwrap(); assert_eq!(tools.len(), 1); assert_eq!(tools[0].function.name, "complex_func"); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); // Verify all 20 parameters are parsed for i in 1..=20 { let key = format!("param{}", i); let expected_value = format!("value{}", i); assert_eq!(args[key], expected_value); } } #[tokio::test] async fn test_minimax_character_by_character_streaming() { // Test character-by-character streaming to simulate real-world streaming let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); let complete_text = r#"Let me help you. Seattle celsius Here are the results."#; let mut content_collected = String::new(); let mut tool_name_found = false; let mut parameters_found = Vec::new(); // Stream character by character - feed only one character at a time for i in 0..complete_text.len() { let delta = &complete_text[i..i + 1]; let result = parser.parse_incremental(delta, &tools).await.unwrap(); content_collected.push_str(&result.normal_text); for call in result.calls { if let Some(name) = call.name { assert_eq!(name, "get_weather"); tool_name_found = true; } if !call.parameters.is_empty() && !parameters_found.contains(&call.parameters) { parameters_found.push(call.parameters.clone()); } } } assert!( tool_name_found, "Should find tool name during character-by-character streaming" ); assert!( !parameters_found.is_empty(), "Should find parameters during streaming" ); // Should have initial content and final content assert!(content_collected.contains("Let me help you.")); assert!(content_collected.contains("Here are the results.")); } #[tokio::test] async fn test_minimax_content_before_and_after_tool_calls() { let parser = MinimaxM2Parser::new(); let input = r#"I'll analyze the weather for you now. Boston MA Based on the analysis, here's what I found."#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); // Verify tool extraction assert_eq!(tools.len(), 1); assert_eq!(tools[0].function.name, "get_weather"); // Verify content preservation (only text before tool call is returned) assert!(normal_text.contains("I'll analyze the weather for you now.")); // Text after tool call is not included in parse_complete assert!(!normal_text.contains("Based on the analysis, here's what I found.")); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["city"], "Boston"); assert_eq!(args["state"], "MA"); } #[tokio::test] async fn test_minimax_incomplete_tool_call() { let parser = MinimaxM2Parser::new(); // Incomplete tool call - missing closing tag let input = r#" Chicago"#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); // Should not extract incomplete tool calls assert_eq!(tools.len(), 0); assert_eq!(normal_text, input); // Should return as normal text } #[tokio::test] async fn test_minimax_malformed_invoke_tag() { let parser = MinimaxM2Parser::new(); // Malformed invoke tag - missing name attribute let input = r#" Miami "#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); // Should not extract tool calls with malformed invoke tags assert_eq!(tools.len(), 0); assert_eq!(normal_text, input); } #[tokio::test] async fn test_minimax_streaming_with_invalid_function_progressive() { let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); // Progressive chunks building an invalid function call let chunks = vec![ "", r#""#, r#"value"#, "", "", ]; let mut all_normal_text = String::new(); let mut found_valid_tool = false; for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); all_normal_text.push_str(&result.normal_text); for call in result.calls { if let Some(name) = call.name { // Should not get here for invalid function if tools.iter().any(|t| t.function.name == name) { found_valid_tool = true; } } } } assert!( !found_valid_tool, "Invalid function should not be parsed as tool call" ); // The invalid tool call should be returned as normal text assert!(all_normal_text.contains("invalid_function")); } #[tokio::test] async fn test_minimax_rapid_streaming_bursts() { // Test handling of rapid streaming bursts (multiple chunks at once) let mut parser = MinimaxM2Parser::new(); let tools = create_test_tools(); let chunks = vec![ "", "rust programming", "", ]; let mut found_function = false; let mut parameters = Vec::new(); for chunk in chunks { let result = parser.parse_incremental(chunk, &tools).await.unwrap(); for call in result.calls { if let Some(name) = call.name { assert_eq!(name, "search"); found_function = true; } if !call.parameters.is_empty() { parameters.push(call.parameters.clone()); } } } assert!(found_function); // Verify that parameters were streamed correctly let final_params = parameters.join(""); assert!(final_params.contains("rust programming")); } #[tokio::test] async fn test_minimax_special_characters_in_values() { let parser = MinimaxM2Parser::new(); let input = r#" Special chars: @#$%^&*() 🦀 Rust 🚀 "double" and 'single' quotes "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); assert_eq!(args["text"], "Special chars: @#$%^&*()"); assert_eq!(args["emoji"], "🦀 Rust 🚀"); assert_eq!(args["quotes"], "\"double\" and 'single' quotes"); } #[tokio::test] async fn test_minimax_whitespace_handling() { let parser = MinimaxM2Parser::new(); // Test with various whitespace scenarios let input = r#" spaces around Line 1 Line 2 tab separated "#; let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); assert_eq!(tools.len(), 1); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); // Values should preserve internal whitespace but may trim edges based on parser design assert!(args["newlines"].as_str().unwrap().contains("Line 1")); assert!(args["newlines"].as_str().unwrap().contains("Line 2")); assert_eq!(args["tabs"], "\ttab\tseparated\t"); } #[tokio::test] async fn test_minimax_no_tools() { // Test input with no tool calls at all let parser = MinimaxM2Parser::new(); let input = r#"This is just a normal response without any tool calls. I can provide information directly without using any tools. Even if I mention function names like get_weather or search, they are not actual tool calls unless properly formatted."#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); // No tools should be extracted assert_eq!( tools.len(), 0, "Should not extract any tools from plain text" ); // All content should be returned as normal text assert_eq!( normal_text, input, "All content should be returned as normal text when no tools present" ); } #[tokio::test] async fn test_minimax_invalid_json_in_parameters() { // Test handling of invalid JSON in parameter values let parser = MinimaxM2Parser::new(); let input = r#" {"key": "value"} {invalid json: no quotes} [1, 2, unclosed Some text {"partial": json} more text "#; let (normal_text, tools) = parser.parse_complete(input).await.unwrap(); // Tool should still be extracted despite invalid JSON in parameters assert_eq!( tools.len(), 1, "Should extract tool even with invalid JSON in parameters" ); assert_eq!(tools[0].function.name, "process"); let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); // Parameters are stored as strings, not parsed as JSON // Even invalid JSON should be preserved as string values assert!(args["valid"].is_string()); assert_eq!(args["valid"], r#"{"key": "value"}"#); assert!(args["invalid"].is_string()); assert_eq!(args["invalid"], "{invalid json: no quotes}"); assert!(args["broken"].is_string()); assert_eq!(args["broken"], "[1, 2, unclosed"); assert!(args["mixed"].is_string()); assert_eq!(args["mixed"], r#"Some text {"partial": json} more text"#); assert_eq!(normal_text, ""); }