Skip to main content

openlark_client/
error.rs

1//! OpenLark Client 错误类型定义
2//!
3//! 基于 openlark-core 的现代化错误处理系统
4//! 直接使用 CoreError,提供类型安全和用户友好的错误管理
5
6use crate::registry::RegistryError;
7use openlark_core::error::{
8    ApiError, CoreError, ErrorCategory, ErrorCode, ErrorContext, ErrorTrait, ErrorType,
9};
10
11/// 🚨 OpenLark 客户端错误类型
12///
13/// 直接类型别名,充分利用 CoreError 的强大功能
14pub type Error = CoreError;
15
16/// 📦 客户端结果类型别名
17pub type Result<T> = std::result::Result<T, Error>;
18
19// ============================================================================
20// 便利错误创建函数(重新导出核心函数)
21// ============================================================================
22
23/// 创建网络错误
24pub fn network_error(message: impl Into<String>) -> Error {
25    openlark_core::error::network_error(message)
26}
27
28/// 创建认证错误
29pub fn authentication_error(message: impl Into<String>) -> Error {
30    openlark_core::error::authentication_error(message)
31}
32
33/// 创建访问令牌格式/内容无效错误
34pub fn token_invalid_error(detail: impl Into<String>) -> Error {
35    openlark_core::error::token_invalid_error(detail)
36}
37
38/// 创建访问令牌过期错误(飞书通用码 99991677)
39pub fn token_expired_error(detail: impl Into<String>) -> Error {
40    openlark_core::error::token_expired_error(detail)
41}
42
43/// 创建缺少权限 scope 的错误
44pub fn permission_missing_error(scopes: &[impl AsRef<str>]) -> Error {
45    openlark_core::error::permission_missing_error(scopes)
46}
47
48/// 创建 SSO 令牌无效错误
49pub fn sso_token_invalid_error(detail: impl Into<String>) -> Error {
50    openlark_core::error::sso_token_invalid_error(detail)
51}
52
53/// 创建身份标识非法错误
54pub fn user_identity_invalid_error(desc: impl Into<String>) -> Error {
55    openlark_core::error::user_identity_invalid_error(desc)
56}
57
58/// 基于飞书通用 `code` 的统一错误映射(客户端自定义解析时可复用)
59pub fn from_feishu_response(
60    code: i32,
61    endpoint: impl Into<String>,
62    message: impl Into<String>,
63    request_id: Option<String>,
64) -> Error {
65    let mapped = ErrorCode::from_feishu_code(code).unwrap_or_else(|| ErrorCode::from_code(code));
66
67    let mut ctx = ErrorContext::new();
68    ctx.add_context("feishu_code", code.to_string());
69    if let Some(req) = request_id {
70        ctx.set_request_id(req);
71    }
72
73    let status = mapped
74        .http_status()
75        .unwrap_or_else(|| match mapped.category() {
76            ErrorCategory::RateLimit => 429,
77            ErrorCategory::Authentication
78            | ErrorCategory::Permission
79            | ErrorCategory::Parameter => 400,
80            ErrorCategory::Resource => 404,
81            _ => 500,
82        });
83
84    CoreError::Api(Box::new(ApiError {
85        status,
86        endpoint: endpoint.into().into(),
87        message: message.into(),
88        source: None,
89        code: mapped,
90        ctx: Box::new(ctx),
91    }))
92}
93
94/// 创建API错误
95pub fn api_error(
96    status: u16,
97    endpoint: impl Into<String>,
98    message: impl Into<String>,
99    request_id: Option<String>,
100) -> Error {
101    openlark_core::error::api_error(status, endpoint, message, request_id)
102}
103
104/// 创建验证错误
105pub fn validation_error(field: impl Into<String>, message: impl Into<String>) -> Error {
106    openlark_core::error::validation_error(field, message)
107}
108
109/// 创建配置错误
110pub fn configuration_error(message: impl Into<String>) -> Error {
111    openlark_core::error::configuration_error(message)
112}
113
114/// 创建序列化错误
115pub fn serialization_error(message: impl Into<String>) -> Error {
116    openlark_core::error::serialization_error(message, None::<serde_json::Error>)
117}
118
119/// 创建业务逻辑错误
120pub fn business_error(_code: impl Into<String>, message: impl Into<String>) -> Error {
121    openlark_core::error::business_error(message)
122}
123
124/// 创建超时错误
125pub fn timeout_error(operation: impl Into<String>) -> Error {
126    use std::time::Duration;
127    openlark_core::error::timeout_error(Duration::from_secs(30), Some(operation.into()))
128}
129
130/// 创建限流错误
131pub fn rate_limit_error(retry_after: Option<u64>) -> Error {
132    use std::time::Duration;
133    openlark_core::error::rate_limit_error(
134        100,
135        Duration::from_secs(60),
136        retry_after.map(Duration::from_secs),
137    )
138}
139
140/// 创建服务不可用错误
141pub fn service_unavailable_error(service: impl Into<String>) -> Error {
142    use std::time::Duration;
143    openlark_core::error::service_unavailable_error(service, Some(Duration::from_secs(60)))
144}
145
146/// 创建内部错误
147pub fn internal_error(message: impl Into<String>) -> Error {
148    openlark_core::error::api_error(500, "internal", message, None::<String>)
149}
150
151/// 创建注册表错误
152pub fn registry_error(err: RegistryError) -> Error {
153    internal_error(format!("服务注册表错误: {err}"))
154}
155
156// ============================================================================
157// 错误扩展功能
158// ============================================================================
159
160/// 客户端错误扩展特征,提供错误恢复建议和步骤
161pub trait ClientErrorExt {
162    /// 获取错误建议
163    fn suggestion(&self) -> &'static str;
164
165    /// 获取错误恢复步骤
166    fn recovery_steps(&self) -> Vec<&'static str>;
167}
168
169impl ClientErrorExt for Error {
170    fn suggestion(&self) -> &'static str {
171        match self.error_type() {
172            ErrorType::Network => "检查网络连接,确认防火墙设置",
173            ErrorType::Authentication => "验证应用凭据,检查令牌有效性",
174            ErrorType::Api => "检查API参数,确认请求格式正确",
175            ErrorType::Validation => "验证输入参数格式和范围",
176            ErrorType::Configuration => "检查应用配置文件和环境变量",
177            ErrorType::Serialization => "确认数据格式正确,检查JSON结构",
178            ErrorType::Business => "确认业务逻辑条件,检查相关权限",
179            ErrorType::Timeout => "增加超时时间或优化请求内容",
180            ErrorType::RateLimit => "稍后重试,考虑降低请求频率",
181            ErrorType::ServiceUnavailable => "稍后重试,检查服务状态",
182            ErrorType::Internal => "联系技术支持,提供错误详情",
183            ErrorType::ResponseTooLarge => "减小请求数据量或增大 max_response_size 配置",
184        }
185    }
186
187    fn recovery_steps(&self) -> Vec<&'static str> {
188        match self.error_type() {
189            ErrorType::Network => vec![
190                "检查网络连接状态",
191                "确认代理设置正确",
192                "验证防火墙规则",
193                "尝试切换网络环境",
194            ],
195            ErrorType::Authentication => vec![
196                "验证应用ID和密钥正确性",
197                "检查令牌是否过期",
198                "确认应用权限配置",
199                "重新生成访问令牌",
200            ],
201            ErrorType::Api => vec![
202                "检查请求参数格式",
203                "确认API端点正确",
204                "验证请求体结构",
205                "查阅API文档",
206            ],
207            ErrorType::Validation => vec![
208                "检查必填字段",
209                "验证数据格式和范围",
210                "确认字段类型正确",
211                "参考输入示例",
212            ],
213            ErrorType::Configuration => vec![
214                "检查环境变量设置",
215                "验证配置文件格式",
216                "确认应用权限配置",
217                "重新加载配置",
218            ],
219            ErrorType::Serialization => vec![
220                "检查JSON格式正确性",
221                "验证数据结构匹配",
222                "确认字段类型一致",
223                "使用在线JSON验证工具",
224            ],
225            ErrorType::Business => vec![
226                "检查业务规则约束",
227                "确认用户权限充分",
228                "验证数据完整性",
229                "联系业务负责人",
230            ],
231            ErrorType::Timeout => vec![
232                "增加请求超时时间",
233                "优化网络环境",
234                "减少请求数据量",
235                "考虑分批处理",
236            ],
237            ErrorType::RateLimit => vec![
238                "等待后重试",
239                "降低请求频率",
240                "实施退避策略",
241                "联系技术支持提高限额",
242            ],
243            ErrorType::ServiceUnavailable => vec![
244                "稍后重试请求",
245                "检查服务状态页面",
246                "切换到备用方案",
247                "联系技术支持",
248            ],
249            ErrorType::Internal => vec![
250                "记录详细错误信息",
251                "检查系统日志",
252                "重启相关服务",
253                "联系技术支持",
254            ],
255            ErrorType::ResponseTooLarge => vec![
256                "减小请求数据量",
257                "增大 max_response_size 配置",
258                "分批请求数据",
259                "联系技术支持确认数据规模",
260            ],
261        }
262    }
263}
264
265// ============================================================================
266// 类型转换
267// ============================================================================
268
269// 注意: reqwest::Error -> CoreError 转换已在 openlark-core 中实现
270// 这里不需要重复实现,直接使用 CoreError 的转换机制
271
272// 注意: 不能为外部类型实现 From,因为这些类型由 CoreError 定义在 openlark-core 中
273// 请使用对应的函数来进行错误转换
274
275// 从注册表错误转换
276impl From<RegistryError> for Error {
277    fn from(err: RegistryError) -> Self {
278        registry_error(err)
279    }
280}
281
282// ============================================================================
283// 便利函数
284// ============================================================================
285
286/// 🔧 从 openlark-core SDKResult 转换为客户端 Result 的便利函数
287///
288/// 这个函数现在只是类型转换,因为我们直接使用 CoreError
289///
290/// # 示例
291///
292/// ```rust
293/// use openlark_client::error::from_sdk_result;
294///
295/// let core_result: openlark_core::SDKResult<String> = Ok("success".to_string());
296/// let client_result = from_sdk_result(core_result);
297/// assert!(client_result.is_ok());
298/// ```
299pub fn from_sdk_result<T>(result: openlark_core::SDKResult<T>) -> Result<T> {
300    result
301}
302
303/// 🔧 创建带有上下文的错误的便利函数
304pub fn with_context<T>(
305    result: Result<T>,
306    context_key: impl Into<String>,
307    context_value: impl Into<String>,
308) -> Result<T> {
309    let key = context_key.into();
310    let value = context_value.into();
311    result.map_err(|err| err.with_context_kv(key, value))
312}
313
314/// 🔧 创建带有操作上下文的错误的便利函数
315pub fn with_operation_context<T>(
316    result: Result<T>,
317    operation: impl Into<String>,
318    component: impl Into<String>,
319) -> Result<T> {
320    let operation = operation.into();
321    let component = component.into();
322    result.map_err(|err| err.with_operation(operation, component))
323}
324
325// ============================================================================
326// 错误分析和报告
327// ============================================================================
328
329/// 错误分析器,提供详细的错误信息和恢复建议
330#[derive(Debug)]
331pub struct ErrorAnalyzer<'a> {
332    error: &'a Error,
333}
334
335impl<'a> ErrorAnalyzer<'a> {
336    /// 创建错误分析器
337    pub fn new(error: &'a Error) -> Self {
338        Self { error }
339    }
340
341    /// 获取详细的错误报告
342    pub fn detailed_report(&self) -> String {
343        let mut report = String::new();
344
345        report.push_str("🚨 错误分析报告\n");
346        report.push_str("================\n\n");
347
348        // 基本信息
349        report.push_str("📋 基本信息:\n");
350        report.push_str(&format!("  错误类型: {:?}\n", self.error.error_type()));
351        report.push_str(&format!("  错误代码: {:?}\n", self.error.error_code()));
352        report.push_str(&format!("  严重程度: {:?}\n", self.error.severity()));
353        report.push_str(&format!("  可重试: {}\n", self.error.is_retryable()));
354
355        if let Some(request_id) = self.error.context().request_id() {
356            report.push_str(&format!("  请求ID: {request_id}\n"));
357        }
358
359        report.push('\n');
360
361        // 错误消息
362        report.push_str("💬 错误消息:\n");
363        report.push_str(&format!("  技术消息: {}\n", self.error));
364        report.push_str(&format!(
365            "  用户消息: {}\n",
366            self.error.user_message().unwrap_or("未知错误")
367        ));
368
369        report.push('\n');
370
371        // 建议和恢复步骤
372        report.push_str("💡 建议:\n");
373        report.push_str(&format!("  {}\n", self.error.suggestion()));
374
375        report.push_str("\n🔧 恢复步骤:\n");
376        for (i, step) in self.error.recovery_steps().iter().enumerate() {
377            report.push_str(&format!("  {}. {}\n", i + 1, step));
378        }
379
380        report.push('\n');
381
382        // 上下文信息
383        if self.error.context().context_len() > 0 {
384            report.push_str("📊 上下文信息:\n");
385            for (key, value) in self.error.context().all_context() {
386                report.push_str(&format!("  {key}: {value}\n"));
387            }
388            report.push('\n');
389        }
390
391        // 时间戳
392        if let Some(timestamp) = self.error.context().timestamp() {
393            report.push_str(&format!("⏰ 发生时间: {timestamp:?}\n"));
394        }
395        report
396    }
397
398    /// 获取适合日志记录的错误摘要
399    pub fn log_summary(&self) -> String {
400        format!(
401            "Error[{:?}:{:?}] {} - {}",
402            self.error.error_type(),
403            self.error.error_code(),
404            self.error.user_message().unwrap_or("未知错误"),
405            if self.error.is_retryable() {
406                "(可重试)"
407            } else {
408                "(不可重试)"
409            }
410        )
411    }
412
413    /// 获取用户友好的错误消息,包含恢复建议
414    pub fn user_friendly_with_suggestion(&self) -> String {
415        format!(
416            "{}\n\n💡 建议: {}\n\n🔧 可以尝试:\n{}",
417            self.error.user_message().unwrap_or("未知错误"),
418            self.error.suggestion(),
419            self.error
420                .recovery_steps()
421                .iter()
422                .enumerate()
423                .map(|(i, step)| format!("{}. {}", i + 1, step))
424                .collect::<Vec<_>>()
425                .join("\n")
426        )
427    }
428}
429
430// 注意: 不能为外部类型 CoreError 定义 inherent impl
431// 请使用 ClientErrorExt trait 来获得扩展功能
432
433// ============================================================================
434// 测试
435// ============================================================================
436
437#[cfg(test)]
438#[allow(unused_imports)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_error_convenience_functions() {
444        let network_err = network_error("连接失败");
445        assert!(network_err.is_network_error());
446        assert!(network_err.is_retryable());
447
448        let auth_err = authentication_error("令牌无效");
449        assert!(auth_err.is_auth_error());
450        assert!(!auth_err.is_retryable());
451
452        let validation_err = validation_error("email", "邮箱格式不正确");
453        assert!(validation_err.is_validation_error());
454        assert!(!validation_err.is_retryable());
455    }
456
457    #[test]
458    fn test_error_analyzer() {
459        let error = api_error(404, "/users", "用户不存在", Some("req-123".to_string()));
460        let analyzer = ErrorAnalyzer::new(&error);
461
462        let report = analyzer.detailed_report();
463        assert!(report.contains("错误分析报告"));
464        assert!(report.contains("API错误"));
465        assert!(report.contains("req-123"));
466
467        let summary = analyzer.log_summary();
468        assert!(summary.contains("Error"));
469        assert!(summary.contains("Api"));
470
471        let user_msg = analyzer.user_friendly_with_suggestion();
472        assert!(user_msg.contains("建议"));
473        assert!(user_msg.contains("可以尝试"));
474    }
475
476    #[test]
477    fn test_client_error_ext() {
478        let error = timeout_error("数据同步");
479
480        assert!(!error.is_network_error());
481        assert!(!error.is_auth_error());
482        assert!(!error.is_business_error());
483        assert!(error.is_retryable());
484
485        let suggestion = error.suggestion();
486        assert!(!suggestion.is_empty());
487
488        let steps = error.recovery_steps();
489        assert!(!steps.is_empty());
490        assert!(steps.contains(&"增加请求超时时间"));
491    }
492
493    #[test]
494    fn test_error_conversions() {
495        // 测试 JSON 错误转换
496        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
497        let error: Error = json_err.into();
498        assert!(error.is_serialization_error());
499
500        // 测试 tokio 超时错误转换
501        // let timeout_err = tokio::time::error::Elapsed {}; // Private field
502        // let error: Error = timeout_err.into();
503        // assert!(error.is_timeout_error());
504        // assert!(error.is_retryable());
505    }
506
507    #[test]
508    fn test_context_functions() {
509        let result: Result<i32> = Err(validation_error("age", "年龄不能为负数"));
510
511        let contextual_result = with_context(result, "user_id", "12345");
512        assert!(contextual_result.is_err());
513
514        let error = contextual_result.unwrap_err();
515        // 我们现在使用结构化上下文,验证上下文内容而不是字符串
516        // assert!(error.to_string().contains("user_id: 12345"));
517        assert_eq!(error.context().get_context("user_id"), Some("12345"));
518    }
519
520    #[test]
521    fn test_sdk_result_conversion() {
522        // 成功情况
523        let core_result: openlark_core::SDKResult<String> = Ok("success".to_string());
524        let client_result: Result<String> = from_sdk_result(core_result);
525        assert!(client_result.is_ok());
526        assert_eq!(client_result.unwrap(), "success");
527
528        // 失败情况
529        let core_result: openlark_core::SDKResult<String> = Err(network_error("网络错误"));
530        let client_result: Result<String> = from_sdk_result(core_result);
531        assert!(client_result.is_err());
532        assert!(client_result.unwrap_err().is_network_error());
533    }
534
535    #[test]
536    fn test_api_error_function() {
537        let error = api_error(
538            500,
539            "/api/test",
540            "服务器内部错误",
541            Some("req-456".to_string()),
542        );
543        assert!(error.is_api_error());
544        let analyzer = ErrorAnalyzer::new(&error);
545        let report = analyzer.detailed_report();
546        assert!(report.contains("服务器内部错误"));
547    }
548
549    #[test]
550    fn test_validation_error_function() {
551        let error = validation_error("field_name", "字段值为空");
552        assert!(error.is_validation_error());
553        let analyzer = ErrorAnalyzer::new(&error);
554        let user_msg = analyzer.user_friendly_with_suggestion();
555        assert!(user_msg.contains("建议"));
556    }
557
558    #[test]
559    fn test_configuration_error_function() {
560        let error = configuration_error("配置文件缺失");
561        assert!(error.is_config_error());
562    }
563
564    #[test]
565    fn test_serialization_error_function() {
566        let error = serialization_error("JSON解析失败");
567        assert!(error.is_serialization_error());
568    }
569
570    #[test]
571    fn test_business_error_function() {
572        let error = business_error("ERR_001", "业务规则验证失败");
573        assert!(error.is_business_error());
574    }
575
576    #[test]
577    fn test_timeout_error_function() {
578        let error = timeout_error("数据库查询超时");
579        assert!(error.is_timeout_error());
580        assert!(error.is_retryable());
581    }
582
583    #[test]
584    fn test_rate_limit_error_function() {
585        let error = rate_limit_error(Some(60));
586        assert!(error.is_rate_limited());
587    }
588
589    #[test]
590    fn test_service_unavailable_error_function() {
591        let error = service_unavailable_error("支付服务");
592        assert!(error.is_service_unavailable_error());
593    }
594
595    #[test]
596    fn test_internal_error_function() {
597        let error = internal_error("系统内部错误");
598        assert!(!error.is_user_error());
599    }
600
601    #[test]
602    fn test_token_invalid_error_function() {
603        let error = token_invalid_error("token格式不正确");
604        assert!(error.is_auth_error());
605    }
606
607    #[test]
608    fn test_token_expired_error_function() {
609        let error = token_expired_error("token已过期");
610        assert!(error.is_auth_error());
611    }
612
613    #[test]
614    fn test_permission_missing_error_function() {
615        let scopes = vec!["read:user", "write:docs"];
616        let error = permission_missing_error(&scopes);
617        assert!(error.is_auth_error());
618    }
619
620    #[test]
621    fn test_sso_token_invalid_error_function() {
622        let error = sso_token_invalid_error("SSO令牌无效");
623        assert!(error.is_auth_error());
624    }
625
626    #[test]
627    fn test_user_identity_invalid_error_function() {
628        let error = user_identity_invalid_error("用户身份标识非法");
629        assert!(error.is_auth_error());
630    }
631
632    #[test]
633    fn test_from_feishu_response_function() {
634        let error = from_feishu_response(
635            99991677,
636            "/api/test",
637            "token过期",
638            Some("req-789".to_string()),
639        );
640        // 错误可能是认证错误或其他类型,只需确保能正确创建
641        assert!(!error.to_string().is_empty());
642        let error2 = from_feishu_response(400, "/api/test", "参数错误", None);
643        assert!(!error2.to_string().is_empty());
644    }
645
646    #[test]
647    fn test_registry_error_conversion() {
648        let registry_err = crate::registry::RegistryError::ServiceNotFound {
649            name: "test".to_string(),
650        };
651        let error: Error = registry_err.into();
652        assert!(!error.is_user_error());
653    }
654
655    #[test]
656    fn test_error_analyzer_log_summary() {
657        let error = network_error("连接超时");
658        let analyzer = ErrorAnalyzer::new(&error);
659        let summary = analyzer.log_summary();
660        assert!(summary.contains("Network"));
661        assert!(summary.contains("可重试") || summary.contains("不可重试"));
662    }
663
664    #[test]
665    fn test_error_analyzer_user_friendly() {
666        let error = api_error(404, "/users/123", "用户不存在", None);
667        let analyzer = ErrorAnalyzer::new(&error);
668        let friendly = analyzer.user_friendly_with_suggestion();
669        assert!(friendly.contains("建议"));
670        assert!(friendly.contains("可以尝试"));
671    }
672
673    #[test]
674    fn test_with_operation_context() {
675        let result: Result<i32> = Err(network_error("网络错误"));
676        let contextual_result = with_operation_context(result, "test_operation", "TestComponent");
677        assert!(contextual_result.is_err());
678        let error = contextual_result.unwrap_err();
679        assert_eq!(
680            error.context().get_context("operation"),
681            Some("test_operation")
682        );
683        assert_eq!(
684            error.context().get_context("component"),
685            Some("TestComponent")
686        );
687    }
688
689    #[test]
690    fn test_with_operation_context_updates_timeout_operation_field() {
691        use std::time::Duration;
692
693        let result: Result<i32> = Err(openlark_core::error::timeout_error(
694            Duration::from_secs(3),
695            Some("old_operation".to_string()),
696        ));
697
698        let contextual_result = with_operation_context(result, "new_operation", "ClientLayer");
699        assert!(contextual_result.is_err());
700
701        match contextual_result.unwrap_err() {
702            CoreError::Timeout {
703                operation, ref ctx, ..
704            } => {
705                assert_eq!(operation.as_deref(), Some("new_operation"));
706                assert_eq!(ctx.operation(), Some("new_operation"));
707                assert_eq!(ctx.component(), Some("ClientLayer"));
708            }
709            other => panic!("expected timeout error, got {:?}", other.error_type()),
710        }
711    }
712
713    #[test]
714    fn test_all_error_types_suggestion() {
715        let error_types = vec![
716            (network_error("test"), "检查网络连接"),
717            (authentication_error("test"), "验证应用凭据"),
718            (api_error(500, "/test", "test", None), "检查API参数"),
719            (validation_error("field", "test"), "验证输入参数"),
720            (configuration_error("test"), "检查应用配置"),
721            (serialization_error("test"), "确认数据格式"),
722            (business_error("code", "test"), "确认业务逻辑"),
723            (timeout_error("test"), "增加超时时间"),
724            (rate_limit_error(None), "稍后重试"),
725            (service_unavailable_error("svc"), "稍后重试"),
726            (internal_error("test"), "联系技术支持"),
727        ];
728
729        for (error, expected_keyword) in error_types {
730            let suggestion = error.suggestion();
731            assert!(
732                suggestion.contains(expected_keyword) || !suggestion.is_empty(),
733                "Error type {:?} should have meaningful suggestion",
734                error.error_type()
735            );
736        }
737    }
738
739    #[test]
740    fn test_all_error_types_recovery_steps() {
741        let error_types = vec![
742            network_error("test"),
743            authentication_error("test"),
744            api_error(500, "/test", "test", None),
745            validation_error("field", "test"),
746            configuration_error("test"),
747            serialization_error("test"),
748            business_error("code", "test"),
749            timeout_error("test"),
750            rate_limit_error(None),
751            service_unavailable_error("svc"),
752            internal_error("test"),
753        ];
754
755        for error in error_types {
756            let steps = error.recovery_steps();
757            assert!(
758                !steps.is_empty(),
759                "Error type {:?} should have recovery steps",
760                error.error_type()
761            );
762            assert!(
763                steps.len() >= 3,
764                "Error type {:?} should have at least 3 recovery steps",
765                error.error_type()
766            );
767        }
768    }
769}