1use serde::Deserialize;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
6pub enum ComposioError {
7 #[error("API error: {message} (status: {status})")]
9 ApiError {
10 status: u16,
12 message: String,
14 code: Option<String>,
16 slug: Option<String>,
18 request_id: Option<String>,
20 suggested_fix: Option<String>,
22 errors: Option<Vec<ErrorDetail>>,
24 },
25
26 #[error("Network error: {0}")]
28 NetworkError(#[from] reqwest::Error),
29
30 #[error("Serialization error: {0}")]
32 SerializationError(#[from] serde_json::Error),
33
34 #[error("Validation error: {0}")]
36 ValidationError(String),
37
38 #[error("Execution error: {0}")]
40 ExecutionError(String),
41
42 #[error("Invalid input: {0}")]
44 InvalidInput(String),
45
46 #[error("Configuration error: {0}")]
48 ConfigError(String),
49
50 #[error("File not found: {0}")]
52 FileNotFound(String),
53
54 #[error("Invalid file: {0}")]
56 InvalidFile(String),
57
58 #[error("File upload failed: {0}")]
60 UploadFailed(String),
61
62 #[error("File download failed: {0}")]
64 DownloadFailed(String),
65
66 #[error("File too large: {0}")]
68 FileTooLarge(String),
69
70 #[error("I/O error: {0}")]
72 IoError(#[from] std::io::Error),
73
74 #[error("Invalid schema: {0}")]
76 InvalidSchema(String),
77
78 #[error("Resource not found: {0}")]
84 NotFound(String),
85
86 #[error("Tool not found: {tool_slug}. Use COMPOSIO_SEARCH_TOOLS to discover available tools")]
88 ToolNotFound {
89 tool_slug: String,
91 },
92
93 #[error("Connected account not found: {account_id}")]
95 ConnectedAccountNotFound {
96 account_id: String,
98 },
99
100 #[error("Invalid connected account: {reason}")]
102 InvalidConnectedAccount {
103 reason: String,
105 },
106
107 #[error("Multiple connected accounts found for user '{user_id}' and toolkit '{toolkit}'. Please specify connected_account_id explicitly")]
109 MultipleConnectedAccounts {
110 user_id: String,
112 toolkit: String,
114 count: usize,
116 },
117
118 #[error("No items found: {0}")]
120 NoItemsFound(String),
121
122 #[error("API Key not provided. Either provide API key or export it as 'COMPOSIO_API_KEY' environment variable")]
128 ApiKeyNotProvided,
129
130 #[error("Invalid API key: {0}")]
132 InvalidApiKey(String),
133
134 #[error("Invalid value '{value}' for enum '{enum_name}'{suggestion}")]
140 InvalidEnum {
141 value: String,
143 enum_name: String,
145 suggestion: String,
147 valid_values: Vec<String>,
149 },
150
151 #[error("Invalid parameters: {0}")]
153 InvalidParams(String),
154
155 #[error("Toolkit version not specified. For manual execution of the tool please pass a specific toolkit version.\n\nPossible fixes:\n1. Pass the toolkit version as a parameter to the execute function ('latest' is not supported in manual execution)\n2. Set the toolkit versions in the Composio config (toolkit_versions={{'<toolkit-slug>': '<toolkit-version>'}})\n3. Set the toolkit version in the environment variable (COMPOSIO_TOOLKIT_VERSION_<TOOLKIT_SLUG>)\n4. Set dangerously_skip_version_check to True (this might cause unexpected behavior when new versions of the tools are released)")]
161 ToolVersionRequired,
162
163 #[error("Error selecting version for tool '{tool}': requested '{requested}', locked '{locked}'")]
165 VersionSelectionError {
166 tool: String,
168 requested: String,
170 locked: String,
172 },
173
174 #[error("Invalid version string: {0}")]
176 InvalidVersionString(String),
177
178 #[error("Lock file error: {0}")]
180 LockFileError(String),
181
182 #[error("Invalid lock file: {0}")]
184 InvalidLockFile(String),
185
186 #[error("Trigger error: {0}")]
192 TriggerError(String),
193
194 #[error("Webhook signature verification failed: {0}")]
196 WebhookSignatureVerificationError(String),
197
198 #[error("Invalid webhook payload: {0}")]
200 WebhookPayloadError(String),
201
202 #[error("Trigger subscription error: {0}")]
204 TriggerSubscriptionError(String),
205
206 #[error("Invalid trigger filters: {0}")]
208 InvalidTriggerFilters(String),
209
210 #[error("Invalid modifier: {0}")]
216 InvalidModifier(String),
217
218 #[error("Tool execution function not set: {0}")]
220 ExecuteToolFnNotSet(String),
221
222 #[error("Error processing tool execution request: {0}")]
224 ErrorProcessingToolExecution(String),
225
226 #[error("Request timeout: {0}")]
232 Timeout(String),
233
234 #[error("Response too large: {size} bytes exceeds maximum of {max_size} bytes")]
236 ResponseTooLarge {
237 size: usize,
239 max_size: usize,
241 },
242
243 #[error("Usage error: {0}")]
249 UsageError(String),
250
251 #[error("Toolkit error: {0}")]
253 ToolkitError(String),
254}
255
256#[derive(Debug, Clone, PartialEq, Deserialize)]
258pub struct ErrorDetail {
259 pub field: Option<String>,
261 pub message: String,
263}
264
265#[derive(Debug, Clone, Deserialize)]
267pub struct ErrorResponse {
268 pub message: String,
270 pub code: Option<String>,
272 pub slug: Option<String>,
274 pub status: u16,
276 pub request_id: Option<String>,
278 pub suggested_fix: Option<String>,
280 pub errors: Option<Vec<ErrorDetail>>,
282}
283
284impl ComposioError {
285 pub async fn from_response(response: reqwest::Response) -> Self {
290 let status = response.status().as_u16();
291
292 match response.json::<ErrorResponse>().await {
293 Ok(err_resp) => ComposioError::ApiError {
294 status,
295 message: err_resp.message,
296 code: err_resp.code,
297 slug: err_resp.slug,
298 request_id: err_resp.request_id,
299 suggested_fix: err_resp.suggested_fix,
300 errors: err_resp.errors,
301 },
302 Err(_) => ComposioError::ApiError {
303 status,
304 message: format!("HTTP error {}", status),
305 code: None,
306 slug: None,
307 request_id: None,
308 suggested_fix: None,
309 errors: None,
310 },
311 }
312 }
313
314 pub fn invalid_enum(value: impl Into<String>, enum_name: impl Into<String>, valid_values: Vec<String>) -> Self {
329 let value = value.into();
330 let enum_name = enum_name.into();
331
332 let suggestion = find_closest_match(&value, &valid_values)
334 .map(|m| format!(". Did you mean '{}'?", m))
335 .unwrap_or_default();
336
337 ComposioError::InvalidEnum {
338 value,
339 enum_name,
340 suggestion,
341 valid_values,
342 }
343 }
344
345 pub fn multiple_connected_accounts(
347 user_id: impl Into<String>,
348 toolkit: impl Into<String>,
349 count: usize,
350 ) -> Self {
351 ComposioError::MultipleConnectedAccounts {
352 user_id: user_id.into(),
353 toolkit: toolkit.into(),
354 count,
355 }
356 }
357
358 pub fn version_selection_error(
360 tool: impl Into<String>,
361 requested: impl Into<String>,
362 locked: impl Into<String>,
363 ) -> Self {
364 ComposioError::VersionSelectionError {
365 tool: tool.into(),
366 requested: requested.into(),
367 locked: locked.into(),
368 }
369 }
370
371 pub fn response_too_large(size: usize, max_size: usize) -> Self {
373 ComposioError::ResponseTooLarge { size, max_size }
374 }
375
376 pub fn tool_not_found(tool_slug: impl Into<String>) -> Self {
378 ComposioError::ToolNotFound {
379 tool_slug: tool_slug.into(),
380 }
381 }
382
383 pub fn connected_account_not_found(account_id: impl Into<String>) -> Self {
385 ComposioError::ConnectedAccountNotFound {
386 account_id: account_id.into(),
387 }
388 }
389
390 pub fn format_validation_error(&self) -> String {
439 match self {
440 ComposioError::ApiError {
441 message,
442 errors,
443 request_id,
444 suggested_fix,
445 ..
446 } => {
447 let mut output = message.clone();
448
449 if let Some(req_id) = request_id {
451 output.push_str(&format!(" (request_id: {})", req_id));
452 }
453
454 if let Some(error_details) = errors {
455 let mut missing_fields = Vec::new();
456 let mut other_errors = Vec::new();
457
458 for detail in error_details {
459 let field_name = detail.field.as_deref().unwrap_or("unknown");
460 let msg_lower = detail.message.to_lowercase();
461
462 if msg_lower.contains("required")
464 || msg_lower.contains("missing")
465 || msg_lower.contains("field is required") {
466 missing_fields.push(field_name);
467 } else {
468 other_errors.push(format!(
469 "{} on parameter `{}`",
470 detail.message, field_name
471 ));
472 }
473 }
474
475 if !missing_fields.is_empty() {
477 output.push_str(&format!(
478 "\n- Following fields are missing: {:?}",
479 missing_fields
480 ));
481 }
482
483 if !other_errors.is_empty() {
485 output.push_str("\n- ");
486 output.push_str(&other_errors.join("\n- "));
487 }
488 }
489
490 if let Some(fix) = suggested_fix {
492 output.push_str(&format!("\n\nSuggested fix: {}", fix));
493 }
494
495 output
496 }
497 ComposioError::ValidationError(msg) => {
498 format!("Validation error: {}", msg)
499 }
500 _ => format!("{}", self),
501 }
502 }
503
504 pub fn is_retryable(&self) -> bool {
517 match self {
518 ComposioError::ApiError { status, .. } => {
519 matches!(status, 429 | 500 | 502 | 503 | 504)
520 }
521 ComposioError::NetworkError(_) => true,
522 ComposioError::Timeout(_) => true,
523 _ => false,
524 }
525 }
526}
527
528fn find_closest_match(target: &str, candidates: &[String]) -> Option<String> {
532 if candidates.is_empty() {
533 return None;
534 }
535
536 let target_lower = target.to_lowercase();
537 let mut best_match: Option<(String, usize)> = None;
538
539 for candidate in candidates {
540 let candidate_lower = candidate.to_lowercase();
541 let distance = levenshtein_distance(&target_lower, &candidate_lower);
542
543 match &best_match {
544 None => best_match = Some((candidate.clone(), distance)),
545 Some((_, best_distance)) if distance < *best_distance => {
546 best_match = Some((candidate.clone(), distance));
547 }
548 _ => {}
549 }
550 }
551
552 best_match.and_then(|(matched, distance)| {
554 let threshold = target.len() / 2;
555 if distance <= threshold {
556 Some(matched)
557 } else {
558 None
559 }
560 })
561}
562
563fn levenshtein_distance(s1: &str, s2: &str) -> usize {
567 let len1 = s1.len();
568 let len2 = s2.len();
569
570 if len1 == 0 {
571 return len2;
572 }
573 if len2 == 0 {
574 return len1;
575 }
576
577 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
578
579 for i in 0..=len1 {
580 matrix[i][0] = i;
581 }
582 for j in 0..=len2 {
583 matrix[0][j] = j;
584 }
585
586 let s1_chars: Vec<char> = s1.chars().collect();
587 let s2_chars: Vec<char> = s2.chars().collect();
588
589 for i in 1..=len1 {
590 for j in 1..=len2 {
591 let cost = if s1_chars[i - 1] == s2_chars[j - 1] { 0 } else { 1 };
592 matrix[i][j] = std::cmp::min(
593 std::cmp::min(
594 matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, ),
597 matrix[i - 1][j - 1] + cost, );
599 }
600 }
601
602 matrix[len1][len2]
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn test_api_error_display() {
611 let error = ComposioError::ApiError {
612 status: 404,
613 message: "Resource not found".to_string(),
614 code: Some("NOT_FOUND".to_string()),
615 slug: Some("resource-not-found".to_string()),
616 request_id: Some("req_123".to_string()),
617 suggested_fix: Some("Check the resource ID".to_string()),
618 errors: None,
619 };
620
621 let display = format!("{}", error);
622 assert!(display.contains("API error"));
623 assert!(display.contains("Resource not found"));
624 assert!(display.contains("404"));
625 }
626
627 #[test]
628 fn test_invalid_input_error() {
629 let error = ComposioError::InvalidInput("Invalid API key".to_string());
630 let display = format!("{}", error);
631 assert!(display.contains("Invalid input"));
632 assert!(display.contains("Invalid API key"));
633 }
634
635 #[test]
636 fn test_config_error() {
637 let error = ComposioError::ConfigError("Invalid base URL".to_string());
638 let display = format!("{}", error);
639 assert!(display.contains("Configuration error"));
640 assert!(display.contains("Invalid base URL"));
641 }
642
643 #[test]
644 fn test_serialization_error_conversion() {
645 let json_error = serde_json::from_str::<serde_json::Value>("invalid json")
646 .unwrap_err();
647 let error: ComposioError = json_error.into();
648
649 match error {
650 ComposioError::SerializationError(_) => (),
651 _ => panic!("Expected SerializationError"),
652 }
653 }
654
655 #[test]
656 fn test_is_retryable_for_rate_limit() {
657 let error = ComposioError::ApiError {
658 status: 429,
659 message: "Rate limited".to_string(),
660 code: None,
661 slug: None,
662 request_id: None,
663 suggested_fix: None,
664 errors: None,
665 };
666
667 assert!(error.is_retryable());
668 }
669
670 #[test]
671 fn test_is_retryable_for_server_errors() {
672 for status in [500, 502, 503, 504] {
673 let error = ComposioError::ApiError {
674 status,
675 message: "Server error".to_string(),
676 code: None,
677 slug: None,
678 request_id: None,
679 suggested_fix: None,
680 errors: None,
681 };
682
683 assert!(
684 error.is_retryable(),
685 "Status {} should be retryable",
686 status
687 );
688 }
689 }
690
691 #[test]
692 fn test_is_not_retryable_for_client_errors() {
693 for status in [400, 401, 403, 404] {
694 let error = ComposioError::ApiError {
695 status,
696 message: "Client error".to_string(),
697 code: None,
698 slug: None,
699 request_id: None,
700 suggested_fix: None,
701 errors: None,
702 };
703
704 assert!(
705 !error.is_retryable(),
706 "Status {} should not be retryable",
707 status
708 );
709 }
710 }
711
712 #[test]
713 fn test_serialization_error_is_not_retryable() {
714 let json_error = serde_json::from_str::<serde_json::Value>("invalid json")
715 .unwrap_err();
716 let error: ComposioError = json_error.into();
717
718 assert!(!error.is_retryable());
719 }
720
721 #[test]
722 fn test_invalid_input_not_retryable() {
723 let error = ComposioError::InvalidInput("Invalid API key".to_string());
724 assert!(!error.is_retryable());
725 }
726
727 #[test]
728 fn test_config_error_not_retryable() {
729 let error = ComposioError::ConfigError("Invalid base URL".to_string());
730 assert!(!error.is_retryable());
731 }
732
733 #[test]
734 fn test_error_detail_deserialization() {
735 let json = r#"{
736 "field": "email",
737 "message": "Invalid email format"
738 }"#;
739
740 let detail: ErrorDetail = serde_json::from_str(json).unwrap();
741 assert_eq!(detail.field, Some("email".to_string()));
742 assert_eq!(detail.message, "Invalid email format");
743 }
744
745 #[test]
746 fn test_error_response_deserialization() {
747 let json = r#"{
748 "message": "Validation failed",
749 "code": "VALIDATION_ERROR",
750 "slug": "validation-failed",
751 "status": 400,
752 "request_id": "req_abc123",
753 "suggested_fix": "Check your input parameters",
754 "errors": [
755 {
756 "field": "user_id",
757 "message": "User ID is required"
758 }
759 ]
760 }"#;
761
762 let response: ErrorResponse = serde_json::from_str(json).unwrap();
763 assert_eq!(response.message, "Validation failed");
764 assert_eq!(response.code, Some("VALIDATION_ERROR".to_string()));
765 assert_eq!(response.status, 400);
766 assert!(response.errors.is_some());
767 assert_eq!(response.errors.as_ref().unwrap().len(), 1);
768 }
769
770 #[test]
771 fn test_error_response_minimal_deserialization() {
772 let json = r#"{
773 "message": "Internal server error",
774 "status": 500
775 }"#;
776
777 let response: ErrorResponse = serde_json::from_str(json).unwrap();
778 assert_eq!(response.message, "Internal server error");
779 assert_eq!(response.status, 500);
780 assert!(response.code.is_none());
781 assert!(response.errors.is_none());
782 }
783
784 #[test]
785 fn test_format_validation_error_with_missing_fields() {
786 let error = ComposioError::ApiError {
787 status: 400,
788 message: "Validation failed".to_string(),
789 code: Some("VALIDATION_ERROR".to_string()),
790 slug: None,
791 request_id: Some("req_abc123".to_string()),
792 suggested_fix: Some("Provide all required fields".to_string()),
793 errors: Some(vec![
794 ErrorDetail {
795 field: Some("user_id".to_string()),
796 message: "Field required".to_string(),
797 },
798 ErrorDetail {
799 field: Some("toolkit".to_string()),
800 message: "This field is required".to_string(),
801 },
802 ]),
803 };
804
805 let formatted = error.format_validation_error();
806
807 assert!(formatted.contains("Validation failed"));
808 assert!(formatted.contains("request_id: req_abc123"));
809 assert!(formatted.contains("Following fields are missing"));
810 assert!(formatted.contains("user_id"));
811 assert!(formatted.contains("toolkit"));
812 assert!(formatted.contains("Suggested fix: Provide all required fields"));
813 }
814
815 #[test]
816 fn test_format_validation_error_with_invalid_values() {
817 let error = ComposioError::ApiError {
818 status: 400,
819 message: "Invalid request data".to_string(),
820 code: Some("VALIDATION_ERROR".to_string()),
821 slug: None,
822 request_id: None,
823 suggested_fix: None,
824 errors: Some(vec![
825 ErrorDetail {
826 field: Some("email".to_string()),
827 message: "Invalid email format".to_string(),
828 },
829 ErrorDetail {
830 field: Some("age".to_string()),
831 message: "Must be a positive integer".to_string(),
832 },
833 ]),
834 };
835
836 let formatted = error.format_validation_error();
837
838 assert!(formatted.contains("Invalid request data"));
839 assert!(formatted.contains("Invalid email format on parameter `email`"));
840 assert!(formatted.contains("Must be a positive integer on parameter `age`"));
841 assert!(!formatted.contains("Following fields are missing"));
842 }
843
844 #[test]
845 fn test_format_validation_error_mixed_errors() {
846 let error = ComposioError::ApiError {
847 status: 400,
848 message: "Validation failed".to_string(),
849 code: Some("VALIDATION_ERROR".to_string()),
850 slug: None,
851 request_id: Some("req_xyz789".to_string()),
852 suggested_fix: Some("Check your input parameters".to_string()),
853 errors: Some(vec![
854 ErrorDetail {
855 field: Some("user_id".to_string()),
856 message: "Field required".to_string(),
857 },
858 ErrorDetail {
859 field: Some("toolkit".to_string()),
860 message: "Invalid toolkit name".to_string(),
861 },
862 ErrorDetail {
863 field: Some("auth_config".to_string()),
864 message: "Missing required field".to_string(),
865 },
866 ]),
867 };
868
869 let formatted = error.format_validation_error();
870
871 assert!(formatted.contains("Following fields are missing"));
873 assert!(formatted.contains("user_id"));
874 assert!(formatted.contains("auth_config"));
875 assert!(formatted.contains("Invalid toolkit name on parameter `toolkit`"));
876 assert!(formatted.contains("Suggested fix: Check your input parameters"));
877 }
878
879 #[test]
880 fn test_format_validation_error_no_field_details() {
881 let error = ComposioError::ApiError {
882 status: 400,
883 message: "Bad request".to_string(),
884 code: None,
885 slug: None,
886 request_id: None,
887 suggested_fix: None,
888 errors: None,
889 };
890
891 let formatted = error.format_validation_error();
892
893 assert_eq!(formatted, "Bad request");
894 }
895
896 #[test]
897 fn test_format_validation_error_for_validation_error_type() {
898 let error = ComposioError::ValidationError("Invalid session configuration".to_string());
899
900 let formatted = error.format_validation_error();
901
902 assert_eq!(formatted, "Validation error: Invalid session configuration");
903 }
904
905 #[test]
906 fn test_format_validation_error_for_other_error_types() {
907 let error = ComposioError::ConfigError("Invalid base URL".to_string());
908
909 let formatted = error.format_validation_error();
910
911 assert!(formatted.contains("Configuration error"));
912 assert!(formatted.contains("Invalid base URL"));
913 }
914
915 #[test]
916 fn test_format_validation_error_with_unknown_field() {
917 let error = ComposioError::ApiError {
918 status: 400,
919 message: "Validation failed".to_string(),
920 code: None,
921 slug: None,
922 request_id: None,
923 suggested_fix: None,
924 errors: Some(vec![
925 ErrorDetail {
926 field: None,
927 message: "Unknown validation error".to_string(),
928 },
929 ]),
930 };
931
932 let formatted = error.format_validation_error();
933
934 assert!(formatted.contains("Unknown validation error on parameter `unknown`"));
935 }
936
937 #[test]
942 fn test_invalid_enum_with_suggestion() {
943 let valid_values = vec!["github".to_string(), "gmail".to_string(), "slack".to_string()];
944 let error = ComposioError::invalid_enum("gihub", "Toolkit", valid_values);
945
946 let display = format!("{}", error);
947 assert!(display.contains("Invalid value 'gihub' for enum 'Toolkit'"));
948 assert!(display.contains("Did you mean 'github'?"));
949 }
950
951 #[test]
952 fn test_invalid_enum_without_close_match() {
953 let valid_values = vec!["github".to_string(), "gmail".to_string(), "slack".to_string()];
954 let error = ComposioError::invalid_enum("xyz123", "Toolkit", valid_values);
955
956 let display = format!("{}", error);
957 assert!(display.contains("Invalid value 'xyz123' for enum 'Toolkit'"));
958 assert!(!display.contains("Did you mean"));
959 }
960
961 #[test]
962 fn test_multiple_connected_accounts_error() {
963 let error = ComposioError::multiple_connected_accounts("user_123", "github", 3);
964
965 let display = format!("{}", error);
966 assert!(display.contains("Multiple connected accounts found"));
967 assert!(display.contains("user_123"));
968 assert!(display.contains("github"));
969 assert!(display.contains("Please specify connected_account_id explicitly"));
970 }
971
972 #[test]
973 fn test_version_selection_error() {
974 let error = ComposioError::version_selection_error("GITHUB_CREATE_ISSUE", "1.0.0", "2.0.0");
975
976 let display = format!("{}", error);
977 assert!(display.contains("Error selecting version"));
978 assert!(display.contains("GITHUB_CREATE_ISSUE"));
979 assert!(display.contains("requested '1.0.0'"));
980 assert!(display.contains("locked '2.0.0'"));
981 }
982
983 #[test]
984 fn test_response_too_large_error() {
985 let error = ComposioError::response_too_large(10_000_000, 5_000_000);
986
987 let display = format!("{}", error);
988 assert!(display.contains("Response too large"));
989 assert!(display.contains("10000000 bytes"));
990 assert!(display.contains("5000000 bytes"));
991 }
992
993 #[test]
994 fn test_tool_not_found_error() {
995 let error = ComposioError::tool_not_found("GITHUB_INVALID_ACTION");
996
997 let display = format!("{}", error);
998 assert!(display.contains("Tool not found: GITHUB_INVALID_ACTION"));
999 assert!(display.contains("COMPOSIO_SEARCH_TOOLS"));
1000 }
1001
1002 #[test]
1003 fn test_connected_account_not_found_error() {
1004 let error = ComposioError::connected_account_not_found("ca_123456");
1005
1006 let display = format!("{}", error);
1007 assert!(display.contains("Connected account not found"));
1008 assert!(display.contains("ca_123456"));
1009 }
1010
1011 #[test]
1012 fn test_api_key_not_provided_error() {
1013 let error = ComposioError::ApiKeyNotProvided;
1014
1015 let display = format!("{}", error);
1016 assert!(display.contains("API Key not provided"));
1017 assert!(display.contains("COMPOSIO_API_KEY"));
1018 }
1019
1020 #[test]
1021 fn test_tool_version_required_error() {
1022 let error = ComposioError::ToolVersionRequired;
1023
1024 let display = format!("{}", error);
1025 assert!(display.contains("Toolkit version not specified"));
1026 assert!(display.contains("Possible fixes"));
1027 assert!(display.contains("dangerously_skip_version_check"));
1028 }
1029
1030 #[test]
1031 fn test_webhook_signature_verification_error() {
1032 let error = ComposioError::WebhookSignatureVerificationError(
1033 "Invalid signature".to_string()
1034 );
1035
1036 let display = format!("{}", error);
1037 assert!(display.contains("Webhook signature verification failed"));
1038 assert!(display.contains("Invalid signature"));
1039 }
1040
1041 #[test]
1042 fn test_timeout_is_retryable() {
1043 let error = ComposioError::Timeout("Request timed out after 30s".to_string());
1044 assert!(error.is_retryable());
1045 }
1046
1047 #[test]
1048 fn test_tool_not_found_not_retryable() {
1049 let error = ComposioError::tool_not_found("INVALID_TOOL");
1050 assert!(!error.is_retryable());
1051 }
1052
1053 #[test]
1054 fn test_invalid_enum_not_retryable() {
1055 let error = ComposioError::invalid_enum(
1056 "invalid",
1057 "TestEnum",
1058 vec!["valid1".to_string(), "valid2".to_string()]
1059 );
1060 assert!(!error.is_retryable());
1061 }
1062
1063 #[test]
1064 fn test_levenshtein_distance() {
1065 use super::levenshtein_distance;
1066
1067 assert_eq!(levenshtein_distance("", ""), 0);
1068 assert_eq!(levenshtein_distance("abc", "abc"), 0);
1069 assert_eq!(levenshtein_distance("abc", "ab"), 1);
1070 assert_eq!(levenshtein_distance("abc", "abcd"), 1);
1071 assert_eq!(levenshtein_distance("github", "gihub"), 1);
1072 assert_eq!(levenshtein_distance("slack", "slak"), 1);
1073 }
1074
1075 #[test]
1076 fn test_find_closest_match() {
1077 use super::find_closest_match;
1078
1079 let candidates = vec![
1080 "github".to_string(),
1081 "gmail".to_string(),
1082 "slack".to_string(),
1083 ];
1084
1085 assert_eq!(
1086 find_closest_match("gihub", &candidates),
1087 Some("github".to_string())
1088 );
1089 assert_eq!(
1090 find_closest_match("gmial", &candidates),
1091 Some("gmail".to_string())
1092 );
1093 assert_eq!(
1094 find_closest_match("slak", &candidates),
1095 Some("slack".to_string())
1096 );
1097
1098 assert_eq!(find_closest_match("xyz123", &candidates), None);
1100
1101 assert_eq!(find_closest_match("test", &[]), None);
1103 }
1104
1105 #[test]
1106 fn test_no_items_found_error() {
1107 let error = ComposioError::NoItemsFound("No toolkits found for query".to_string());
1108
1109 let display = format!("{}", error);
1110 assert!(display.contains("No items found"));
1111 assert!(display.contains("No toolkits found"));
1112 }
1113
1114 #[test]
1115 fn test_invalid_trigger_filters_error() {
1116 let error = ComposioError::InvalidTriggerFilters(
1117 "Invalid filter: unknown_field".to_string()
1118 );
1119
1120 let display = format!("{}", error);
1121 assert!(display.contains("Invalid trigger filters"));
1122 assert!(display.contains("unknown_field"));
1123 }
1124
1125 #[test]
1126 fn test_invalid_modifier_error() {
1127 let error = ComposioError::InvalidModifier(
1128 "Modifier 'before_execute' not found".to_string()
1129 );
1130
1131 let display = format!("{}", error);
1132 assert!(display.contains("Invalid modifier"));
1133 assert!(display.contains("before_execute"));
1134 }
1135}