use anyhow::Result; use docx_mcp::docx_tools::DocxToolsProvider; use docx_mcp::security::SecurityConfig; use mcp_core::types::ToolResponseContent; use serde_json::{json, Value}; use tempfile::TempDir; use std::collections::HashSet; use std::fs; use std::path::PathBuf; use pretty_assertions::assert_eq; // tokio_test not needed in async tests here enum ToolResult { Success(Value), Error(String), } async fn tool_result(provider: &DocxToolsProvider, name: &str, args: Value) -> ToolResult { let resp = provider.call_tool(name, args).await; let val = match resp.content.get(0) { Some(ToolResponseContent::Text(t)) => serde_json::from_str::(&t.text) .unwrap_or_else(|_| json!({"success": false, "error": t.text.clone()})), _ => json!({"success": false, "error": "non-text response"}), }; if val.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { ToolResult::Success(val) } else { let err = val.get("error").and_then(|v| v.as_str()).unwrap_or("Unknown error").to_string(); ToolResult::Error(err) } } /// Test complete document creation workflow from start to finish #[tokio::test] async fn test_complete_document_workflow() -> Result<()> { let temp_dir = TempDir::new().unwrap(); let provider = DocxToolsProvider::with_base_dir(temp_dir.path()); // Step 1: Create a new document let create_result = tool_result(&provider, "create_document", json!({})).await; let doc_id = match create_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); value["document_id"].as_str().unwrap().to_string() }, ToolResult::Error(e) => panic!("Failed to create document: {}", e), }; // Step 2: Add document structure let title_result = tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Annual Report 2024", "level": 1 })).await; assert!(matches!(title_result, ToolResult::Success(_)), "add_heading failed at start"); // Step 3: Add introduction let intro_result = tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": "This annual report provides a comprehensive overview of our company's performance, achievements, and strategic direction for the year 2024.", "style": { "font_size": 12, "alignment": "justify" } })).await; assert!(matches!(intro_result, ToolResult::Success(_))); // Step 4: Add executive summary section let exec_heading_result = tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Executive Summary", "level": 2 })).await; assert!(matches!(exec_heading_result, ToolResult::Success(_))); let exec_content = tool_result(&provider, "add_list", json!({ "document_id": doc_id, "items": [ "Record revenue growth of 15% year-over-year", "Successful expansion into three new markets", "Launch of five innovative products", "Achievement of carbon neutrality goals", "Increased employee satisfaction by 20%" ], "ordered": false })).await; assert!(matches!(exec_content, ToolResult::Success(_))); // Step 5: Add financial data table let financial_heading = tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Financial Highlights", "level": 2 })).await; assert!(matches!(financial_heading, ToolResult::Success(_))); let table_result = tool_result(&provider, "add_table", json!({ "document_id": doc_id, "rows": [ ["Metric", "2023", "2024", "Change"], ["Revenue ($M)", "120.5", "138.6", "+15%"], ["Operating Income ($M)", "24.1", "29.3", "+22%"], ["Net Income ($M)", "18.2", "22.7", "+25%"], ["Employees", "1,250", "1,420", "+14%"] ] })).await; assert!(matches!(table_result, ToolResult::Success(_))); // Step 6: Add page break and new section let page_break_result = tool_result(&provider, "add_page_break", json!({ "document_id": doc_id })).await; assert!(matches!(page_break_result, ToolResult::Success(_))); let strategy_heading = tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Strategic Initiatives", "level": 2 })).await; assert!(matches!(strategy_heading, ToolResult::Success(_))); // Step 7: Add multiple paragraphs with different styles let bold_paragraph = tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": "Digital Transformation: Our commitment to digital innovation remains at the forefront of our strategic priorities.", "style": { "bold": true, "font_size": 13 } })).await; assert!(matches!(bold_paragraph, ToolResult::Success(_))); let regular_paragraph = tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": "Throughout 2024, we have invested significantly in technology infrastructure, data analytics capabilities, and employee digital skills development. This comprehensive approach has resulted in improved operational efficiency and enhanced customer experience across all touchpoints." })).await; assert!(matches!(regular_paragraph, ToolResult::Success(_))); // Step 8: Set document header and footer let header_result = tool_result(&provider, "set_header", json!({ "document_id": doc_id, "text": "Annual Report 2024 | Confidential" })).await; assert!(matches!(header_result, ToolResult::Success(_))); let footer_result = tool_result(&provider, "set_footer", json!({ "document_id": doc_id, "text": "© 2024 Company Name. All rights reserved." })).await; assert!(matches!(footer_result, ToolResult::Success(_))); // Step 9: Verify document content let extract_result = tool_result(&provider, "extract_text", json!({ "document_id": doc_id })).await; match extract_result { ToolResult::Success(value) => { let text = value["text"].as_str().unwrap(); // Verify all content is present assert!(text.contains("Annual Report 2024")); assert!(text.contains("Executive Summary")); assert!(text.contains("Record revenue growth")); assert!(text.contains("Financial Highlights")); assert!(text.contains("Revenue ($M)")); assert!(text.contains("138.6")); assert!(text.contains("Strategic Initiatives")); assert!(text.contains("Digital Transformation")); println!("Document contains {} characters of text", text.len()); assert!(text.len() > 600, "Document should have substantial content"); }, ToolResult::Error(e) => panic!("Failed to extract text: {}", e), } // Step 10: Get document metadata let metadata_result = tool_result(&provider, "get_metadata", json!({ "document_id": doc_id })).await; match metadata_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let metadata = &value["metadata"]; assert_eq!(metadata["id"], doc_id); assert!(metadata["path"].is_string()); }, ToolResult::Error(e) => panic!("Failed to get metadata: {}", e), } // Step 11: Export to different formats let output_dir = temp_dir.path().join("exports"); fs::create_dir_all(&output_dir)?; // Export to PDF let pdf_path = output_dir.join("annual_report.pdf"); let pdf_result = tool_result(&provider, "convert_to_pdf", json!({ "document_id": doc_id, "output_path": pdf_path.to_str().unwrap() })).await; assert!(matches!(pdf_result, ToolResult::Success(_))); assert!(pdf_path.exists()); // Export to markdown let md_path = output_dir.join("annual_report.md"); let md_result = tool_result(&provider, "export_to_markdown", json!({ "document_id": doc_id, "output_path": md_path.to_str().unwrap() })).await; assert!(matches!(md_result, ToolResult::Success(_))); assert!(md_path.exists()); // Step 12: Save the original document let save_path = output_dir.join("annual_report.docx"); let save_result = tool_result(&provider, "save_document", json!({ "document_id": doc_id, "output_path": save_path.to_str().unwrap() })).await; assert!(matches!(save_result, ToolResult::Success(_))); assert!(save_path.exists()); println!("Complete workflow test successful! Generated files:"); println!("- PDF: {:?}", pdf_path); println!("- Markdown: {:?}", md_path); println!("- DOCX: {:?}", save_path); Ok(()) } /// Test document editing and revision workflow #[tokio::test] async fn test_document_editing_workflow() -> Result<()> { let temp_dir = TempDir::new().unwrap(); let provider = DocxToolsProvider::with_base_dir(temp_dir.path()); // Create initial document let create_result = tool_result(&provider, "create_document", json!({})).await; let doc_id = match create_result { ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(), _ => panic!("Failed to create document"), }; // Add initial content tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Project Status Report", "level": 1 })).await; tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": "Current project status and upcoming milestones." })).await; // Add tasks list tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Current Tasks", "level": 2 })).await; tool_result(&provider, "add_list", json!({ "document_id": doc_id, "items": [ "Complete user interface design", "Implement backend API", "Write unit tests", "Deploy to staging environment" ], "ordered": true })).await; // Search for specific content let search_result = tool_result(&provider, "search_text", json!({ "document_id": doc_id, "search_term": "backend", "case_sensitive": false })).await; match search_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let matches = value["matches"].as_array().unwrap(); assert!(!matches.is_empty()); assert!(value["total_matches"].as_u64().unwrap() > 0); }, ToolResult::Error(e) => panic!("Search failed: {}", e), } // Get word count before modifications let word_count_before = tool_result(&provider, "get_word_count", json!({ "document_id": doc_id })).await; let initial_word_count = match word_count_before { ToolResult::Success(value) => { value["statistics"]["words"].as_u64().unwrap() }, _ => panic!("Failed to get word count"), }; // Add more content (simulating document expansion) tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Completed Items", "level": 2 })).await; tool_result(&provider, "add_table", json!({ "document_id": doc_id, "rows": [ ["Task", "Completed Date", "Notes"], ["Requirements gathering", "2024-01-15", "All stakeholders interviewed"], ["Architecture design", "2024-01-22", "Approved by tech committee"], ["Database schema", "2024-01-28", "Optimized for performance"] ] })).await; // Add risks section tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": "Identified Risks", "level": 2 })).await; tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": "The following risks have been identified and mitigation strategies are in place:", "style": { "italic": true } })).await; tool_result(&provider, "add_list", json!({ "document_id": doc_id, "items": [ "Resource constraints may delay delivery", "Third-party API changes could impact integration", "Security requirements may require additional development time" ], "ordered": false })).await; // Get word count after modifications let word_count_after = tool_result(&provider, "get_word_count", json!({ "document_id": doc_id })).await; let final_word_count = match word_count_after { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let stats = &value["statistics"]; let words = stats["words"].as_u64().unwrap(); let chars = stats["characters"].as_u64().unwrap(); let sentences = stats["sentences"].as_u64().unwrap(); println!("Document statistics: {} words, {} characters, {} sentences", words, chars, sentences); assert!(words > 0); assert!(chars > 0); assert!(sentences > 0); words }, ToolResult::Error(e) => panic!("Failed to get final word count: {}", e), }; // Verify document grew assert!(final_word_count > initial_word_count, "Document should have more words after additions: {} -> {}", initial_word_count, final_word_count); // Perform find and replace operation let replace_result = tool_result(&provider, "find_and_replace", json!({ "document_id": doc_id, "find_text": "backend", "replace_text": "server-side", "case_sensitive": false })).await; match replace_result { ToolResult::Success(value) => { // Note: The actual implementation might return different result structure println!("Find and replace completed: {:?}", value); }, ToolResult::Error(_) => { // This is acceptable as find_and_replace might not be fully implemented println!("Find and replace not fully implemented yet"); } } // Final verification let final_text = tool_result(&provider, "extract_text", json!({ "document_id": doc_id })).await; match final_text { ToolResult::Success(value) => { let text = value["text"].as_str().unwrap(); // Verify all sections are present assert!(text.contains("Project Status Report")); assert!(text.contains("Current Tasks")); assert!(text.contains("Completed Items")); assert!(text.contains("Identified Risks")); assert!(text.contains("Requirements gathering")); assert!(text.contains("Resource constraints")); println!("Final document contains {} characters", text.len()); }, ToolResult::Error(e) => panic!("Failed to extract final text: {}", e), } Ok(()) } /// Test collaborative workflow with multiple document operations #[tokio::test] async fn test_collaborative_workflow() -> Result<()> { let temp_dir = TempDir::new().unwrap(); let provider = DocxToolsProvider::with_base_dir(temp_dir.path()); let mut document_ids = Vec::new(); // Simulate multiple team members creating documents let team_members = vec!["Alice", "Bob", "Charlie"]; for member in &team_members { // Each member creates a document let create_result = tool_result(&provider, "create_document", json!({})).await; let doc_id = match create_result { ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(), _ => panic!("Failed to create document for {}", member), }; // Add member-specific content tool_result(&provider, "add_heading", json!({ "document_id": doc_id, "text": format!("{}'s Weekly Report", member), "level": 1 })).await; tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": format!("This week, {} focused on the following activities and achievements.", member) })).await; // Add achievements let achievements = match member { &"Alice" => vec![ "Completed user research interviews", "Created wireframes for new features", "Updated design system documentation" ], &"Bob" => vec![ "Implemented new API endpoints", "Optimized database queries", "Fixed critical security vulnerability" ], &"Charlie" => vec![ "Deployed version 2.1 to production", "Set up monitoring dashboards", "Conducted security audit" ], _ => vec!["General tasks completed"], }; provider.call_tool("add_list", json!({ "document_id": doc_id, "items": achievements, "ordered": false })).await; // Add metrics table provider.call_tool("add_heading", json!({ "document_id": doc_id, "text": "Key Metrics", "level": 2 })).await; let metrics = match member { &"Alice" => vec![ vec!["Interviews Conducted", "8"], vec!["Designs Created", "12"], vec!["User Stories", "15"] ], &"Bob" => vec![ vec!["Lines of Code", "2,450"], vec!["Tests Written", "23"], vec!["Bugs Fixed", "7"] ], &"Charlie" => vec![ vec!["Deployments", "3"], vec!["Issues Resolved", "11"], vec!["System Uptime", "99.9%"] ], _ => vec![vec!["Tasks", "5"]], }; let mut table_rows = vec![vec!["Metric".to_string(), "Value".to_string()]]; for metric in metrics { table_rows.push(metric.iter().map(|s| s.to_string()).collect()); } provider.call_tool("add_table", json!({ "document_id": doc_id, "rows": table_rows })).await; document_ids.push((member.to_string(), doc_id)); } // List all documents let list_result = tool_result(&provider, "list_documents", json!({})).await; match list_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let documents = value["documents"].as_array().unwrap(); assert!(documents.len() >= 3, "Should have at least 3 documents"); println!("Found {} documents in the system", documents.len()); }, ToolResult::Error(e) => panic!("Failed to list documents: {}", e), } // Generate a summary document combining all reports let summary_result = tool_result(&provider, "create_document", json!({})).await; let summary_id = match summary_result { ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(), ToolResult::Error(e) => panic!("Failed to create summary document: {}", e), }; // Add summary header tool_result(&provider, "add_heading", json!({ "document_id": summary_id, "text": "Team Weekly Summary Report", "level": 1 })).await; tool_result(&provider, "add_paragraph", json!({ "document_id": summary_id, "text": "This document summarizes the key activities and achievements from all team members this week." })).await; // Add content from each team member's document for (member, doc_id) in &document_ids { tool_result(&provider, "add_heading", json!({ "document_id": summary_id, "text": format!("{} Highlights", member), "level": 2 })).await; // Extract text from member's document let extract_result = tool_result(&provider, "extract_text", json!({ "document_id": doc_id })).await; match extract_result { ToolResult::Success(value) => { let text = value["text"].as_str().unwrap(); // Extract key points (simplified - would be more sophisticated in real implementation) let lines: Vec<&str> = text.lines().collect(); let summary_text = if lines.len() > 10 { format!("Key activities include multiple achievements in their focus areas. Full details available in {}'s individual report.", member) } else { format!("Summary content from {}'s report.", member) }; tool_result(&provider, "add_paragraph", json!({ "document_id": summary_id, "text": summary_text })).await; }, ToolResult::Error(e) => { println!("Warning: Could not extract text from {}'s document: {}", member, e); } } } // Add team totals table tool_result(&provider, "add_heading", json!({ "document_id": summary_id, "text": "Team Totals", "level": 2 })).await; tool_result(&provider, "add_table", json!({ "document_id": summary_id, "rows": [ ["Team Member", "Documents Created", "Key Focus"], ["Alice", "1", "Design & Research"], ["Bob", "1", "Development & Security"], ["Charlie", "1", "Operations & Deployment"], ["Total", "3", "Full-stack delivery"] ] })).await; // Convert all documents to PDF for archival let archive_dir = temp_dir.path().join("weekly_archive"); fs::create_dir_all(&archive_dir)?; for (member, doc_id) in &document_ids { let pdf_path = archive_dir.join(format!("{}_weekly_report.pdf", member.to_lowercase())); tool_result(&provider, "convert_to_pdf", json!({ "document_id": doc_id, "output_path": pdf_path.to_str().unwrap() })).await; if pdf_path.exists() { println!("Archived {}'s report to PDF", member); } } // Archive summary document let summary_pdf = archive_dir.join("team_summary.pdf"); tool_result(&provider, "convert_to_pdf", json!({ "document_id": summary_id, "output_path": summary_pdf.to_str().unwrap() })).await; // Verify all PDFs were created let pdf_count = fs::read_dir(&archive_dir)? .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("pdf")) .count(); assert!(pdf_count >= 3, "Should have created at least 3 PDF files"); println!("Successfully archived {} PDF documents", pdf_count); Ok(()) } /// Test security-restricted workflow #[tokio::test] async fn test_security_restricted_workflow() -> Result<()> { let temp_dir = TempDir::new().unwrap(); // Create a restrictive security configuration let mut whitelist = HashSet::new(); whitelist.insert("open_document".to_string()); whitelist.insert("extract_text".to_string()); whitelist.insert("get_metadata".to_string()); whitelist.insert("search_text".to_string()); whitelist.insert("get_word_count".to_string()); whitelist.insert("list_documents".to_string()); whitelist.insert("get_security_info".to_string()); let security_config = SecurityConfig { readonly_mode: true, sandbox_mode: true, command_whitelist: Some(whitelist), command_blacklist: None, max_document_size: 1024 * 1024, // 1MB max_open_documents: 5, allow_external_tools: false, allow_network: false, }; let provider = DocxToolsProvider::with_base_dir_and_security(temp_dir.path(), security_config); // Test security info let security_info = tool_result(&provider, "get_security_info", json!({})).await; match security_info { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let security = &value["security"]; assert_eq!(security["readonly_mode"], true); assert_eq!(security["sandbox_mode"], true); println!("Security configuration: {}", security["summary"].as_str().unwrap()); }, ToolResult::Error(e) => panic!("Failed to get security info: {}", e), } // Test that write operations are blocked let create_result = tool_result(&provider, "create_document", json!({})).await; match create_result { ToolResult::Success(value) => { // Should fail security check assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap()); }, ToolResult::Error(e) => { assert!(e.contains("Security check failed") || e.contains("Command not allowed")); println!("Create document correctly blocked: {}", e); } } // Test that add_paragraph is blocked let paragraph_result = tool_result(&provider, "add_paragraph", json!({ "document_id": "test", "text": "This should be blocked" })).await; match paragraph_result { ToolResult::Success(value) => { assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap()); }, ToolResult::Error(e) => { assert!(e.contains("Security check failed") || e.contains("Command not allowed")); println!("Add paragraph correctly blocked: {}", e); } } // Create a test document externally (outside security restrictions) let unrestricted_provider = DocxToolsProvider::new(); let create_result = tool_result(&unrestricted_provider, "create_document", json!({})).await; let doc_id = match create_result { ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(), _ => panic!("Failed to create test document"), }; // Add content with unrestricted provider tool_result(&unrestricted_provider, "add_heading", json!({ "document_id": doc_id, "text": "Security Test Document", "level": 1 })).await; tool_result(&unrestricted_provider, "add_paragraph", json!({ "document_id": doc_id, "text": "This document is used to test readonly access capabilities in a security-restricted environment." })).await; tool_result(&unrestricted_provider, "add_list", json!({ "document_id": doc_id, "items": [ "Test text extraction", "Test search functionality", "Test metadata retrieval", "Test word counting" ], "ordered": true })).await; // Save document to a sandbox-allowed path and reopen it under restricted provider // Use OS temp dir root to satisfy sandbox canonicalization let saved_path = std::env::temp_dir().join("docx-mcp").join("restricted_source.docx"); std::fs::create_dir_all(saved_path.parent().unwrap()).unwrap(); tool_result(&unrestricted_provider, "save_document", json!({ "document_id": doc_id, "output_path": saved_path.to_str().unwrap() })).await; // Open under restricted provider to import into its registry let opened = tool_result(&provider, "open_document", json!({ "path": saved_path.to_str().unwrap() })).await; let doc_id = match opened { ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(), ToolResult::Error(e) => panic!("Restricted provider failed to open saved document: {}", e), }; // Now test readonly operations with restricted provider // These should work because they're in the whitelist // Test text extraction let extract_result = tool_result(&provider, "extract_text", json!({ "document_id": doc_id })).await; match extract_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let text = value["text"].as_str().unwrap(); assert!(text.contains("Security Test Document")); assert!(text.contains("Test text extraction")); println!("Text extraction successful: {} characters", text.len()); }, ToolResult::Error(e) => panic!("Text extraction should work: {}", e), } // Test search functionality let search_result = tool_result(&provider, "search_text", json!({ "document_id": doc_id, "search_term": "security", "case_sensitive": false })).await; match search_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); assert!(value["total_matches"].as_u64().unwrap() > 0); println!("Search successful: found {} matches", value["total_matches"]); }, ToolResult::Error(e) => panic!("Search should work: {}", e), } // Test metadata retrieval let metadata_result = tool_result(&provider, "get_metadata", json!({ "document_id": doc_id })).await; match metadata_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let metadata = &value["metadata"]; assert_eq!(metadata["id"], doc_id); println!("Metadata retrieval successful"); }, ToolResult::Error(e) => panic!("Metadata retrieval should work: {}", e), } // Test word counting let word_count_result = tool_result(&provider, "get_word_count", json!({ "document_id": doc_id })).await; match word_count_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); let stats = &value["statistics"]; assert!(stats["words"].as_u64().unwrap() > 0); println!("Word count successful: {} words", stats["words"]); }, ToolResult::Error(e) => panic!("Word count should work: {}", e), } // Test document listing let list_result = tool_result(&provider, "list_documents", json!({})).await; match list_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); println!("Document listing successful"); }, ToolResult::Error(e) => panic!("Document listing should work: {}", e), } // Test that conversion operations are blocked (not in whitelist) let pdf_result = tool_result(&provider, "convert_to_pdf", json!({ "document_id": doc_id, "output_path": "/tmp/test.pdf" })).await; match pdf_result { ToolResult::Success(value) => { assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap()); }, ToolResult::Error(e) => { assert!(e.contains("Security check failed") || e.contains("Command not allowed")); println!("PDF conversion correctly blocked: {}", e); } } println!("Security-restricted workflow test completed successfully"); Ok(()) } /// Test error recovery workflow #[tokio::test] async fn test_error_recovery_workflow() -> Result<()> { let temp_dir = TempDir::new().unwrap(); let provider = DocxToolsProvider::with_base_dir(temp_dir.path()); // Test recovery from invalid document ID let invalid_ops = vec![ ("extract_text", json!({"document_id": "nonexistent-123"})), ("add_paragraph", json!({"document_id": "fake-456", "text": "test"})), ("get_metadata", json!({"document_id": "invalid-789"})), ("get_word_count", json!({"document_id": "missing-000"})), ]; for (operation, args) in invalid_ops { let result = tool_result(&provider, operation, args).await; match result { ToolResult::Success(value) => { assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap()); println!("{} correctly handled invalid document ID (structured)", operation); }, ToolResult::Error(e) => { // Any error is acceptable for invalid IDs across operations println!("{} correctly returned error for invalid document: {}", operation, e); } } } // Test recovery from invalid arguments let invalid_arg_ops = vec![ ("add_heading", json!({"document_id": "test", "level": 10})), // Invalid level ("add_paragraph", json!({"text": "missing document_id"})), // Missing required field ("add_table", json!({"document_id": "test", "rows": "not_an_array"})), // Wrong type ]; for (operation, args) in invalid_arg_ops { let result = tool_result(&provider, operation, args).await; match result { ToolResult::Success(value) => { assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap()); println!("{} handled invalid arguments gracefully", operation); }, ToolResult::Error(e) => { println!("{} returned error for invalid arguments: {}", operation, e); } } } // Test successful operation after errors let create_result = tool_result(&provider, "create_document", json!({})).await; let doc_id = match create_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); value["document_id"].as_str().unwrap().to_string() }, ToolResult::Error(e) => panic!("Should be able to create document after errors: {}", e), }; // Verify normal operations work after handling errors let paragraph_result = tool_result(&provider, "add_paragraph", json!({ "document_id": doc_id, "text": "This should work after error recovery" })).await; match paragraph_result { ToolResult::Success(value) => { assert!(value["success"].as_bool().unwrap()); println!("Normal operations work after error handling"); }, ToolResult::Error(e) => panic!("Normal operation should work after errors: {}", e), } // Test that the document has the expected content let extract_result = tool_result(&provider, "extract_text", json!({ "document_id": doc_id })).await; match extract_result { ToolResult::Success(value) => { let text = value["text"].as_str().unwrap(); assert!(text.contains("This should work after error recovery")); println!("Error recovery workflow completed successfully"); }, ToolResult::Error(e) => panic!("Text extraction failed: {}", e), } Ok(()) }