1use crate::llm::provider::LLMError;
8use crate::retry_after::retry_after_from_llm_metadata;
9use crate::tools::registry::{ToolErrorType, ToolExecutionError};
10use crate::tools::unified_error::{UnifiedErrorKind, UnifiedToolError};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13use vtcode_commons::sanitizer::sanitize_provider_diagnostic;
14pub use vtcode_commons::{
15 BackoffStrategy, ConfigGuidance, ErrorCategory, MisconfigurationKind, Retryability, detect_misconfiguration,
16 is_misconfiguration,
17};
18use vtcode_macros::DebugNoInline;
19
20pub type Result<T> = std::result::Result<T, VtCodeError>;
22
23#[derive(DebugNoInline, Error, Serialize, Deserialize)]
33#[error("{category}: {message}")]
34pub struct VtCodeError {
35 pub category: ErrorCategory,
37
38 pub code: ErrorCode,
40
41 pub message: String,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub context: Option<String>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub retry_after_ms: Option<u64>,
51
52 #[serde(skip)]
54 #[source]
55 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub enum ErrorCode {
61 InvalidArgument,
63 ValidationFailed,
64 ParseError,
65
66 CommandFailed,
68 ToolExecutionFailed,
69 Timeout,
70
71 ConnectionFailed,
73 RequestFailed,
74 RateLimited,
75 ServiceUnavailable,
76
77 AuthenticationFailed,
79 LLMProviderError,
80 TokenLimitExceeded,
81 ContextTooLong,
82
83 ConfigInvalid,
85 ConfigMissing,
86 ConfigParseFailed,
87
88 PermissionDenied,
90 PolicyViolation,
91 PlanningPolicyViolation,
92 SandboxViolation,
93 DotfileProtection,
94
95 IoError,
97 OutOfMemory,
98 ResourceUnavailable,
99 ResourceNotFound,
100
101 ToolNotFound,
103 CircuitOpen,
104 Cancelled,
105 Unexpected,
106 NotImplemented,
107}
108
109impl VtCodeError {
110 pub fn new<S: Into<String>>(category: ErrorCategory, code: ErrorCode, message: S) -> Self {
112 Self {
113 category,
114 code,
115 message: message.into(),
116 context: None,
117 retry_after_ms: None,
118 source: None,
119 }
120 }
121
122 pub fn with_context<S: Into<String>>(mut self, context: S) -> Self {
124 self.context = Some(context.into());
125 self
126 }
127
128 pub fn with_retry_after(mut self, retry_after: std::time::Duration) -> Self {
130 self.retry_after_ms = Some(retry_after.as_millis().min(u128::from(u64::MAX)) as u64);
131 self
132 }
133
134 pub fn with_source<E: std::error::Error + Send + Sync + 'static>(mut self, source: E) -> Self {
136 self.source = Some(Box::new(source));
137 self
138 }
139
140 pub fn retry_after(&self) -> Option<std::time::Duration> {
142 self.retry_after_ms.map(std::time::Duration::from_millis)
143 }
144
145 pub const fn is_retryable(&self) -> bool {
147 self.category.is_retryable()
148 }
149
150 pub fn retryability(&self) -> Retryability {
152 self.category.retryability()
153 }
154
155 pub fn input<S: Into<String>>(code: ErrorCode, message: S) -> Self {
157 Self::new(ErrorCategory::InvalidParameters, code, message)
158 }
159
160 pub fn execution<S: Into<String>>(code: ErrorCode, message: S) -> Self {
162 Self::new(ErrorCategory::ExecutionError, code, message)
163 }
164
165 pub fn network<S: Into<String>>(code: ErrorCode, message: S) -> Self {
167 Self::new(ErrorCategory::Network, code, message)
168 }
169
170 pub fn llm<S: Into<String>>(code: ErrorCode, message: S) -> Self {
172 Self::new(ErrorCategory::ExecutionError, code, message)
173 }
174
175 pub fn config<S: Into<String>>(code: ErrorCode, message: S) -> Self {
177 Self::new(ErrorCategory::InvalidParameters, code, message)
178 }
179
180 pub fn security<S: Into<String>>(code: ErrorCode, message: S) -> Self {
182 Self::new(ErrorCategory::PolicyViolation, code, message)
183 }
184
185 pub fn system<S: Into<String>>(code: ErrorCode, message: S) -> Self {
187 Self::new(ErrorCategory::ExecutionError, code, message)
188 }
189
190 pub fn internal<S: Into<String>>(code: ErrorCode, message: S) -> Self {
192 Self::new(ErrorCategory::ExecutionError, code, message)
193 }
194
195 pub fn from_category<S: Into<String>>(category: ErrorCategory, message: S) -> Self {
197 Self::new(category, ErrorCode::from_category(category), message)
198 }
199
200 #[must_use]
209 pub fn misconfiguration_guidance(&self) -> Option<ConfigGuidance> {
210 if matches!(self.code, ErrorCode::ConfigInvalid | ErrorCode::ConfigMissing | ErrorCode::ConfigParseFailed) {
211 if let Some(guidance) = self.message_guidance() {
212 return Some(guidance);
213 }
214 return Some(ConfigGuidance {
215 kind: MisconfigurationKind::ConfigFile,
216 setting: "vtcode.toml",
217 location: "workspace / user / system config layers",
218 fix: std::borrow::Cow::Borrowed(
219 "Invalid config file. Validate vtcode.toml syntax and fields, then retry.",
220 ),
221 });
222 }
223 self.message_guidance()
224 }
225
226 #[must_use]
229 pub fn is_misconfiguration(&self) -> bool {
230 self.misconfiguration_guidance().is_some()
231 }
232
233 #[must_use]
237 pub fn with_misconfiguration_guidance(mut self) -> Self {
238 let Some(guidance) = self.misconfiguration_guidance() else {
239 return self;
240 };
241 let suffix = guidance.user_message();
242 if self.message.contains("Correct the configuration before retrying.") {
245 return self;
246 }
247 self.message.push(' ');
251 self.message.push_str(&suffix);
252 self
253 }
254
255 fn message_guidance(&self) -> Option<ConfigGuidance> {
256 let mut haystack = self.message.clone();
257 if let Some(context) = self.context.as_deref() {
258 haystack.push('\n');
259 haystack.push_str(context);
260 }
261 let mut source = self.source.as_deref().map(|source| source as &dyn std::error::Error);
262 while let Some(error) = source {
263 haystack.push('\n');
264 haystack.push_str(&error.to_string());
265 source = error.source();
266 }
267 detect_misconfiguration(self.category, &haystack)
268 }
269}
270
271impl ErrorCode {
272 pub const fn from_category(category: ErrorCategory) -> Self {
274 match category {
275 ErrorCategory::Network => ErrorCode::ConnectionFailed,
276 ErrorCategory::Timeout => ErrorCode::Timeout,
277 ErrorCategory::RateLimit => ErrorCode::RateLimited,
278 ErrorCategory::ServiceUnavailable => ErrorCode::ServiceUnavailable,
279 ErrorCategory::CircuitOpen => ErrorCode::CircuitOpen,
280 ErrorCategory::Authentication => ErrorCode::AuthenticationFailed,
281 ErrorCategory::InvalidParameters => ErrorCode::InvalidArgument,
282 ErrorCategory::ToolNotFound => ErrorCode::ToolNotFound,
283 ErrorCategory::ResourceNotFound => ErrorCode::ResourceNotFound,
284 ErrorCategory::PermissionDenied => ErrorCode::PermissionDenied,
285 ErrorCategory::PolicyViolation => ErrorCode::PolicyViolation,
286 ErrorCategory::PlanningPolicyViolation => ErrorCode::PlanningPolicyViolation,
287 ErrorCategory::SandboxFailure => ErrorCode::SandboxViolation,
288 ErrorCategory::ResourceExhausted => ErrorCode::ResourceUnavailable,
289 ErrorCategory::Cancelled => ErrorCode::Cancelled,
290 ErrorCategory::ExecutionError => ErrorCode::Unexpected,
291 }
292 }
293
294 fn from_unified_kind(kind: UnifiedErrorKind) -> Self {
295 match kind {
296 UnifiedErrorKind::Timeout => ErrorCode::Timeout,
297 UnifiedErrorKind::Network => ErrorCode::ConnectionFailed,
298 UnifiedErrorKind::RateLimit => ErrorCode::RateLimited,
299 UnifiedErrorKind::ArgumentValidation => ErrorCode::ValidationFailed,
300 UnifiedErrorKind::ToolNotFound => ErrorCode::ToolNotFound,
301 UnifiedErrorKind::PermissionDenied => ErrorCode::PermissionDenied,
302 UnifiedErrorKind::SandboxFailure => ErrorCode::SandboxViolation,
303 UnifiedErrorKind::InternalError => ErrorCode::Unexpected,
304 UnifiedErrorKind::CircuitOpen => ErrorCode::CircuitOpen,
305 UnifiedErrorKind::ResourceExhausted => ErrorCode::ResourceUnavailable,
306 UnifiedErrorKind::Cancelled => ErrorCode::Cancelled,
307 UnifiedErrorKind::PolicyViolation => ErrorCode::PolicyViolation,
308 UnifiedErrorKind::PlanningPolicyViolation => ErrorCode::PlanningPolicyViolation,
309 UnifiedErrorKind::ExecutionFailed | UnifiedErrorKind::Unknown => ErrorCode::ToolExecutionFailed,
310 }
311 }
312
313 fn from_tool_error_type(error_type: ToolErrorType) -> Self {
314 match error_type {
315 ToolErrorType::InvalidParameters => ErrorCode::ValidationFailed,
316 ToolErrorType::ToolNotFound => ErrorCode::ToolNotFound,
317 ToolErrorType::PermissionDenied => ErrorCode::PermissionDenied,
318 ToolErrorType::ResourceNotFound => ErrorCode::ResourceNotFound,
319 ToolErrorType::NetworkError => ErrorCode::ConnectionFailed,
320 ToolErrorType::Timeout => ErrorCode::Timeout,
321 ToolErrorType::ExecutionError => ErrorCode::ToolExecutionFailed,
322 ToolErrorType::PolicyViolation => ErrorCode::PolicyViolation,
323 }
324 }
325}
326
327impl From<std::io::Error> for VtCodeError {
329 fn from(err: std::io::Error) -> Self {
330 VtCodeError::system(ErrorCode::IoError, err.to_string())
331 .with_source(err)
332 .with_misconfiguration_guidance()
333 }
334}
335
336impl From<serde_json::Error> for VtCodeError {
337 fn from(err: serde_json::Error) -> Self {
338 VtCodeError::config(ErrorCode::ConfigParseFailed, err.to_string())
339 .with_source(err)
340 .with_misconfiguration_guidance()
341 }
342}
343
344impl From<reqwest::Error> for VtCodeError {
345 fn from(err: reqwest::Error) -> Self {
346 let code = if err.is_timeout() {
347 ErrorCode::Timeout
348 } else if err.is_connect() {
349 ErrorCode::ConnectionFailed
350 } else {
351 ErrorCode::RequestFailed
352 };
353 VtCodeError::network(code, err.to_string())
354 .with_source(err)
355 .with_misconfiguration_guidance()
356 }
357}
358
359impl From<anyhow::Error> for VtCodeError {
360 fn from(err: anyhow::Error) -> Self {
361 let category = vtcode_commons::classify_anyhow_error(&err);
362 VtCodeError::new(category, ErrorCode::from_category(category), err.to_string())
363 .with_context(format!("{err:#}"))
364 .with_misconfiguration_guidance()
365 }
366}
367
368impl From<LLMError> for VtCodeError {
369 fn from(err: LLMError) -> Self {
370 let category = ErrorCategory::from(&err);
371 let code = match &err {
372 LLMError::Authentication { .. } => ErrorCode::AuthenticationFailed,
373 LLMError::RateLimit { .. } => {
374 if category == ErrorCategory::ResourceExhausted {
375 ErrorCode::from_category(category)
376 } else {
377 ErrorCode::RateLimited
378 }
379 }
380 LLMError::InvalidRequest { .. } => ErrorCode::ValidationFailed,
381 LLMError::Network { message, .. } => {
382 if vtcode_commons::classify_error_message(message) == ErrorCategory::Timeout {
383 ErrorCode::Timeout
384 } else {
385 ErrorCode::ConnectionFailed
386 }
387 }
388 LLMError::Provider { metadata, .. } => {
389 if category == ErrorCategory::ResourceExhausted {
390 ErrorCode::from_category(category)
391 } else {
392 metadata
393 .as_ref()
394 .and_then(|meta| meta.status)
395 .map(|status| match status {
396 408 => ErrorCode::Timeout,
397 429 => ErrorCode::RateLimited,
398 500 | 502 | 503 | 504 | 529 => ErrorCode::ServiceUnavailable,
399 _ => ErrorCode::LLMProviderError,
400 })
401 .unwrap_or(ErrorCode::LLMProviderError)
402 }
403 }
404 };
405 let message = llm_error_message(&err);
406 let metadata_context = llm_metadata_context(&err);
407 let retry_after = llm_retry_after(&err);
408
409 let mut error = VtCodeError::new(category, code, message);
410 if let Some(context) = metadata_context {
411 error = error.with_context(context);
412 }
413 let error = error.with_source(err);
414 let error = if let Some(retry_after) = retry_after {
415 error.with_retry_after(retry_after)
416 } else {
417 error
418 };
419 error.with_misconfiguration_guidance()
420 }
421}
422
423impl From<UnifiedToolError> for VtCodeError {
424 fn from(err: UnifiedToolError) -> Self {
425 let mut error = VtCodeError::new(
426 ErrorCategory::from(err.kind),
427 ErrorCode::from_unified_kind(err.kind),
428 err.user_message.clone(),
429 );
430
431 if let Some(ctx) = &err.debug_context {
432 let mut metadata = vec![format!("tool={}", ctx.tool_name), format!("attempt={}", ctx.attempt)];
433 if let Some(invocation_id) = &ctx.invocation_id {
434 metadata.push(format!("invocation_id={invocation_id}"));
435 }
436 metadata.extend(ctx.metadata.iter().map(|(key, value)| format!("{key}={value}")));
437 error = error.with_context(metadata.join(", "));
438 }
439
440 error.with_source(err).with_misconfiguration_guidance()
441 }
442}
443
444impl From<ToolExecutionError> for VtCodeError {
445 fn from(err: ToolExecutionError) -> Self {
446 let category = ErrorCategory::from(err.error_type);
447 let mut error =
448 VtCodeError::new(category, ErrorCode::from_tool_error_type(err.error_type), err.message.clone());
449
450 let mut context_parts = Vec::new();
451 if let Some(original_error) = &err.original_error {
452 context_parts.push(format!("original_error={original_error}"));
453 }
454 if !err.recovery_suggestions.is_empty() {
455 context_parts.push(format!("recovery_suggestions={}", err.recovery_suggestions.join(" | ")));
456 }
457 if !context_parts.is_empty() {
458 error = error.with_context(context_parts.join(", "));
459 }
460
461 error.with_misconfiguration_guidance()
462 }
463}
464
465fn llm_error_message(error: &LLMError) -> String {
466 match error {
467 LLMError::Authentication { message, .. }
468 | LLMError::InvalidRequest { message, .. }
469 | LLMError::Network { message, .. }
470 | LLMError::Provider { message, .. } => message.clone(),
471 LLMError::RateLimit { metadata } => metadata
472 .as_ref()
473 .and_then(|meta| meta.message.clone())
474 .unwrap_or_else(|| "rate limit exceeded".to_string()),
475 }
476}
477
478fn llm_metadata_context(error: &LLMError) -> Option<String> {
479 let metadata = match error {
480 LLMError::Authentication { metadata, .. }
481 | LLMError::RateLimit { metadata }
482 | LLMError::InvalidRequest { metadata, .. }
483 | LLMError::Network { metadata, .. }
484 | LLMError::Provider { metadata, .. } => metadata.as_deref(),
485 }?;
486
487 let mut context = Vec::new();
488 if let Some(code) = metadata.code.as_deref() {
489 context.push(format!("provider_code={}", sanitize_provider_diagnostic(code.as_bytes())));
490 }
491 if let Some(message) = metadata.message.as_deref() {
492 context.push(format!("provider_message={}", sanitize_provider_diagnostic(message.as_bytes())));
493 }
494 (!context.is_empty()).then(|| context.join(", "))
495}
496
497fn llm_retry_after(error: &LLMError) -> Option<std::time::Duration> {
498 let metadata = match error {
499 LLMError::Authentication { metadata, .. }
500 | LLMError::RateLimit { metadata }
501 | LLMError::InvalidRequest { metadata, .. }
502 | LLMError::Network { metadata, .. }
503 | LLMError::Provider { metadata, .. } => metadata.as_ref(),
504 }?;
505
506 retry_after_from_llm_metadata(metadata)
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::llm::provider::LLMErrorMetadata;
513 use crate::tools::unified_error::DebugContext;
514
515 #[test]
516 fn test_error_creation() {
517 let err = VtCodeError::input(ErrorCode::InvalidArgument, "Invalid argument");
518 assert_eq!(err.category, ErrorCategory::InvalidParameters);
519 assert_eq!(err.code, ErrorCode::InvalidArgument);
520 assert_eq!(err.message, "Invalid argument");
521 }
522
523 #[test]
524 fn test_error_with_context() {
525 let err =
526 VtCodeError::input(ErrorCode::InvalidArgument, "Invalid argument").with_context("While parsing user input");
527 assert_eq!(err.context, Some("While parsing user input".to_string()));
528 }
529
530 #[test]
531 fn test_error_with_source() {
532 let io_err = std::io::Error::other("IO error");
533 let err = VtCodeError::system(ErrorCode::IoError, "File operation failed").with_source(io_err);
534 assert!(err.source.is_some());
535 }
536
537 #[test]
538 fn test_error_category_display() {
539 let err = VtCodeError::network(ErrorCode::ConnectionFailed, "Connection failed");
540 let display = format!("{err}");
541 assert!(display.contains("Network error"));
542 assert!(display.contains("Connection failed"));
543 }
544
545 #[test]
546 fn test_error_serialization_skips_source() {
547 let io_err = std::io::Error::other("IO error");
548 let err = VtCodeError::system(ErrorCode::IoError, "File operation failed")
549 .with_context("While reading config")
550 .with_source(io_err);
551
552 let json = serde_json::to_string(&err).expect("vtcode error should serialize");
553 assert!(json.contains("\"message\":\"File operation failed\""));
554 assert!(json.contains("\"context\":\"While reading config\""));
555 assert!(!json.contains("source"));
556 }
557
558 #[test]
559 fn test_error_with_retry_after() {
560 let err = VtCodeError::network(ErrorCode::RateLimited, "rate limit")
561 .with_retry_after(std::time::Duration::from_secs(2));
562 assert_eq!(err.retry_after(), Some(std::time::Duration::from_secs(2)));
563 }
564
565 #[test]
566 fn test_llm_error_conversion_preserves_retry_after() {
567 let err = LLMError::RateLimit {
568 metadata: Some(LLMErrorMetadata::new(
569 "OpenAI",
570 Some(429),
571 Some("rate_limit".to_string()),
572 Some("req-1".to_string()),
573 None,
574 Some("3".to_string()),
575 Some("try again later".to_string()),
576 )),
577 };
578
579 let converted = VtCodeError::from(err);
580 assert_eq!(converted.category, ErrorCategory::RateLimit);
581 assert_eq!(converted.code, ErrorCode::RateLimited);
582 assert_eq!(converted.retry_after(), Some(std::time::Duration::from_secs(3)));
583 }
584
585 #[test]
586 fn test_llm_metadata_marks_converted_error_as_misconfiguration() {
587 let err = LLMError::Provider {
588 message: "provider request failed".to_string(),
589 metadata: Some(LLMErrorMetadata::new(
590 "OpenAI",
591 Some(404),
592 Some("model_not_found".to_string()),
593 None,
594 None,
595 None,
596 Some("The requested model does not exist".to_string()),
597 )),
598 };
599
600 let converted = VtCodeError::from(err);
601 assert!(converted.is_misconfiguration());
602 assert!(converted.message.contains("Check settings/config first"));
603 assert!(
604 converted
605 .context
606 .as_deref()
607 .is_some_and(|context| context.contains("provider_code=model_not_found"))
608 );
609 }
610
611 #[test]
612 fn test_llm_error_conversion_preserves_fractional_retry_after() {
613 let err = LLMError::RateLimit {
614 metadata: Some(LLMErrorMetadata::new(
615 "OpenAI",
616 Some(429),
617 Some("rate_limit".to_string()),
618 Some("req-1".to_string()),
619 None,
620 Some("0.5".to_string()),
621 Some("try again later".to_string()),
622 )),
623 };
624
625 let converted = VtCodeError::from(err);
626 assert_eq!(converted.retry_after(), Some(std::time::Duration::from_millis(500)));
627 }
628
629 #[test]
630 fn test_llm_quota_exhaustion_uses_resource_exhausted_code() {
631 let err = LLMError::RateLimit {
632 metadata: Some(LLMErrorMetadata::new(
633 "OpenAI",
634 Some(429),
635 Some("insufficient_quota".to_string()),
636 None,
637 None,
638 None,
639 Some("quota exceeded".to_string()),
640 )),
641 };
642
643 let converted = VtCodeError::from(err);
644 assert_eq!(converted.category, ErrorCategory::ResourceExhausted);
645 assert_eq!(converted.code, ErrorCode::ResourceUnavailable);
646 }
647
648 #[test]
649 fn test_unified_tool_error_conversion_preserves_context() {
650 let err = UnifiedToolError::new(UnifiedErrorKind::Network, "network down").with_context(DebugContext {
651 tool_name: "read_file".to_string(),
652 invocation_id: Some("inv-1".to_string()),
653 attempt: 2,
654 metadata: vec![("duration_ms".to_string(), "1500".to_string())],
655 });
656
657 let converted = VtCodeError::from(err);
658 assert_eq!(converted.category, ErrorCategory::Network);
659 assert_eq!(converted.code, ErrorCode::ConnectionFailed);
660 assert!(converted.context.as_deref().is_some_and(|ctx| ctx.contains("tool=read_file")));
661 }
662
663 #[test]
664 fn test_unified_tool_source_marks_converted_error_as_misconfiguration() {
665 let err = UnifiedToolError::new(UnifiedErrorKind::Network, "provider request failed")
666 .with_source(anyhow::anyhow!("unknown model 'gpt-99' in agent.model"));
667
668 let converted = VtCodeError::from(err);
669 assert!(converted.is_misconfiguration());
670 assert!(converted.message.contains("Check settings/config first"));
671 }
672
673 #[test]
674 fn test_tool_execution_error_conversion_uses_original_context() {
675 let err = ToolExecutionError::with_original_error(
676 "command_session".to_string(),
677 ToolErrorType::Timeout,
678 "Tool execution failed".to_string(),
679 "timed out waiting for process".to_string(),
680 );
681
682 let converted = VtCodeError::from(err);
683 assert_eq!(converted.category, ErrorCategory::Timeout);
684 assert_eq!(converted.code, ErrorCode::Timeout);
685 assert!(
686 converted
687 .context
688 .as_deref()
689 .is_some_and(|ctx| ctx.contains("original_error=timed out waiting for process"))
690 );
691 }
692
693 #[test]
694 fn test_misconfiguration_guidance_for_auth() {
695 let err = VtCodeError::new(
696 ErrorCategory::Authentication,
697 ErrorCode::AuthenticationFailed,
698 "Authentication failed: invalid api key",
699 );
700 assert!(err.is_misconfiguration());
701 let guided = err.with_misconfiguration_guidance();
702 assert!(guided.message.contains("Check settings/config first"));
703 assert!(guided.message.contains("before retrying"));
704 }
705
706 #[test]
707 fn test_misconfiguration_guidance_for_config_code() {
708 let err = VtCodeError::config(ErrorCode::ConfigInvalid, "custom_providers[x]: `base_url` must not be empty");
709 assert!(err.is_misconfiguration());
710 }
711
712 #[test]
713 fn test_transient_has_no_misconfiguration() {
714 let err = VtCodeError::network(ErrorCode::ConnectionFailed, "connection reset by peer");
715 assert!(!err.is_misconfiguration());
716 let guided = err.with_misconfiguration_guidance();
717 assert!(!guided.message.contains("Check settings/config first"));
718 }
719
720 #[test]
721 fn test_misconfiguration_is_idempotent() {
722 let err = VtCodeError::new(ErrorCategory::Authentication, ErrorCode::AuthenticationFailed, "bad key")
723 .with_misconfiguration_guidance()
724 .with_misconfiguration_guidance();
725 assert_eq!(err.message.matches("Correct the configuration before retrying.").count(), 1);
726 }
727
728 #[test]
729 fn test_misconfiguration_guidance_does_not_echo_secrets() {
730 let secret = concat!("sk-", "test1234567890abcdef");
731 let err = VtCodeError::new(
732 ErrorCategory::Authentication,
733 ErrorCode::AuthenticationFailed,
734 format!("Authentication failed: invalid api key {secret}"),
735 );
736 let guidance = err.misconfiguration_guidance().expect("auth must match");
737 assert!(!guidance.user_message().contains(secret));
738 }
739
740 #[test]
741 fn test_misconfiguration_guidance_stays_visible_for_large_errors() {
742 let mut message = "x".repeat(8 * 1024);
743 message.push_str(" invalid api key");
744 let guided = VtCodeError::execution(ErrorCode::Unexpected, message).with_misconfiguration_guidance();
745 assert!(guided.message.contains("Check settings/config first"));
746 assert!(guided.to_string().contains("Correct the configuration before retrying."));
747 }
748}