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 = error.metadata()?;
480
481 let mut context = Vec::new();
482 if let Some(code) = metadata.code.as_deref() {
483 context.push(format!("provider_code={}", sanitize_provider_diagnostic(code.as_bytes())));
484 }
485 if let Some(message) = metadata.message.as_deref() {
486 context.push(format!("provider_message={}", sanitize_provider_diagnostic(message.as_bytes())));
487 }
488 (!context.is_empty()).then(|| context.join(", "))
489}
490
491fn llm_retry_after(error: &LLMError) -> Option<std::time::Duration> {
492 let metadata = error.metadata()?;
493
494 retry_after_from_llm_metadata(metadata)
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::llm::provider::LLMErrorMetadata;
501 use crate::tools::unified_error::DebugContext;
502
503 #[test]
504 fn test_error_creation() {
505 let err = VtCodeError::input(ErrorCode::InvalidArgument, "Invalid argument");
506 assert_eq!(err.category, ErrorCategory::InvalidParameters);
507 assert_eq!(err.code, ErrorCode::InvalidArgument);
508 assert_eq!(err.message, "Invalid argument");
509 }
510
511 #[test]
512 fn test_error_with_context() {
513 let err =
514 VtCodeError::input(ErrorCode::InvalidArgument, "Invalid argument").with_context("While parsing user input");
515 assert_eq!(err.context, Some("While parsing user input".to_string()));
516 }
517
518 #[test]
519 fn test_error_with_source() {
520 let io_err = std::io::Error::other("IO error");
521 let err = VtCodeError::system(ErrorCode::IoError, "File operation failed").with_source(io_err);
522 assert!(err.source.is_some());
523 }
524
525 #[test]
526 fn test_error_category_display() {
527 let err = VtCodeError::network(ErrorCode::ConnectionFailed, "Connection failed");
528 let display = format!("{err}");
529 assert!(display.contains("Network error"));
530 assert!(display.contains("Connection failed"));
531 }
532
533 #[test]
534 fn test_error_serialization_skips_source() {
535 let io_err = std::io::Error::other("IO error");
536 let err = VtCodeError::system(ErrorCode::IoError, "File operation failed")
537 .with_context("While reading config")
538 .with_source(io_err);
539
540 let json = serde_json::to_string(&err).expect("vtcode error should serialize");
541 assert!(json.contains("\"message\":\"File operation failed\""));
542 assert!(json.contains("\"context\":\"While reading config\""));
543 assert!(!json.contains("source"));
544 }
545
546 #[test]
547 fn test_error_with_retry_after() {
548 let err = VtCodeError::network(ErrorCode::RateLimited, "rate limit")
549 .with_retry_after(std::time::Duration::from_secs(2));
550 assert_eq!(err.retry_after(), Some(std::time::Duration::from_secs(2)));
551 }
552
553 #[test]
554 fn test_llm_error_conversion_preserves_retry_after() {
555 let err = LLMError::RateLimit {
556 metadata: Some(LLMErrorMetadata::new(
557 "OpenAI",
558 Some(429),
559 Some("rate_limit".to_string()),
560 Some("req-1".to_string()),
561 None,
562 Some("3".to_string()),
563 Some("try again later".to_string()),
564 )),
565 };
566
567 let converted = VtCodeError::from(err);
568 assert_eq!(converted.category, ErrorCategory::RateLimit);
569 assert_eq!(converted.code, ErrorCode::RateLimited);
570 assert_eq!(converted.retry_after(), Some(std::time::Duration::from_secs(3)));
571 }
572
573 #[test]
574 fn test_llm_metadata_marks_converted_error_as_misconfiguration() {
575 let err = LLMError::Provider {
576 message: "provider request failed".to_string(),
577 metadata: Some(LLMErrorMetadata::new(
578 "OpenAI",
579 Some(404),
580 Some("model_not_found".to_string()),
581 None,
582 None,
583 None,
584 Some("The requested model does not exist".to_string()),
585 )),
586 };
587
588 let converted = VtCodeError::from(err);
589 assert!(converted.is_misconfiguration());
590 assert!(converted.message.contains("Check settings/config first"));
591 assert!(
592 converted
593 .context
594 .as_deref()
595 .is_some_and(|context| context.contains("provider_code=model_not_found"))
596 );
597 }
598
599 #[test]
600 fn test_llm_error_conversion_preserves_fractional_retry_after() {
601 let err = LLMError::RateLimit {
602 metadata: Some(LLMErrorMetadata::new(
603 "OpenAI",
604 Some(429),
605 Some("rate_limit".to_string()),
606 Some("req-1".to_string()),
607 None,
608 Some("0.5".to_string()),
609 Some("try again later".to_string()),
610 )),
611 };
612
613 let converted = VtCodeError::from(err);
614 assert_eq!(converted.retry_after(), Some(std::time::Duration::from_millis(500)));
615 }
616
617 #[test]
618 fn test_llm_quota_exhaustion_uses_resource_exhausted_code() {
619 let err = LLMError::RateLimit {
620 metadata: Some(LLMErrorMetadata::new(
621 "OpenAI",
622 Some(429),
623 Some("insufficient_quota".to_string()),
624 None,
625 None,
626 None,
627 Some("quota exceeded".to_string()),
628 )),
629 };
630
631 let converted = VtCodeError::from(err);
632 assert_eq!(converted.category, ErrorCategory::ResourceExhausted);
633 assert_eq!(converted.code, ErrorCode::ResourceUnavailable);
634 }
635
636 #[test]
637 fn test_unified_tool_error_conversion_preserves_context() {
638 let err = UnifiedToolError::new(UnifiedErrorKind::Network, "network down").with_context(DebugContext {
639 tool_name: "read_file".to_string(),
640 invocation_id: Some("inv-1".to_string()),
641 attempt: 2,
642 metadata: vec![("duration_ms".to_string(), "1500".to_string())],
643 });
644
645 let converted = VtCodeError::from(err);
646 assert_eq!(converted.category, ErrorCategory::Network);
647 assert_eq!(converted.code, ErrorCode::ConnectionFailed);
648 assert!(converted.context.as_deref().is_some_and(|ctx| ctx.contains("tool=read_file")));
649 }
650
651 #[test]
652 fn test_unified_tool_source_marks_converted_error_as_misconfiguration() {
653 let err = UnifiedToolError::new(UnifiedErrorKind::Network, "provider request failed")
654 .with_source(anyhow::anyhow!("unknown model 'gpt-99' in agent.model"));
655
656 let converted = VtCodeError::from(err);
657 assert!(converted.is_misconfiguration());
658 assert!(converted.message.contains("Check settings/config first"));
659 }
660
661 #[test]
662 fn test_tool_execution_error_conversion_uses_original_context() {
663 let err = ToolExecutionError::with_original_error(
664 "command_session".to_string(),
665 ToolErrorType::Timeout,
666 "Tool execution failed".to_string(),
667 "timed out waiting for process".to_string(),
668 );
669
670 let converted = VtCodeError::from(err);
671 assert_eq!(converted.category, ErrorCategory::Timeout);
672 assert_eq!(converted.code, ErrorCode::Timeout);
673 assert!(
674 converted
675 .context
676 .as_deref()
677 .is_some_and(|ctx| ctx.contains("original_error=timed out waiting for process"))
678 );
679 }
680
681 #[test]
682 fn test_misconfiguration_guidance_for_auth() {
683 let err = VtCodeError::new(
684 ErrorCategory::Authentication,
685 ErrorCode::AuthenticationFailed,
686 "Authentication failed: invalid api key",
687 );
688 assert!(err.is_misconfiguration());
689 let guided = err.with_misconfiguration_guidance();
690 assert!(guided.message.contains("Check settings/config first"));
691 assert!(guided.message.contains("before retrying"));
692 }
693
694 #[test]
695 fn test_misconfiguration_guidance_for_config_code() {
696 let err = VtCodeError::config(ErrorCode::ConfigInvalid, "custom_providers[x]: `base_url` must not be empty");
697 assert!(err.is_misconfiguration());
698 }
699
700 #[test]
701 fn test_transient_has_no_misconfiguration() {
702 let err = VtCodeError::network(ErrorCode::ConnectionFailed, "connection reset by peer");
703 assert!(!err.is_misconfiguration());
704 let guided = err.with_misconfiguration_guidance();
705 assert!(!guided.message.contains("Check settings/config first"));
706 }
707
708 #[test]
709 fn test_misconfiguration_is_idempotent() {
710 let err = VtCodeError::new(ErrorCategory::Authentication, ErrorCode::AuthenticationFailed, "bad key")
711 .with_misconfiguration_guidance()
712 .with_misconfiguration_guidance();
713 assert_eq!(err.message.matches("Correct the configuration before retrying.").count(), 1);
714 }
715
716 #[test]
717 fn test_misconfiguration_guidance_does_not_echo_secrets() {
718 let secret = concat!("sk-", "test1234567890abcdef");
719 let err = VtCodeError::new(
720 ErrorCategory::Authentication,
721 ErrorCode::AuthenticationFailed,
722 format!("Authentication failed: invalid api key {secret}"),
723 );
724 let guidance = err.misconfiguration_guidance().expect("auth must match");
725 assert!(!guidance.user_message().contains(secret));
726 }
727
728 #[test]
729 fn test_misconfiguration_guidance_stays_visible_for_large_errors() {
730 let mut message = "x".repeat(8 * 1024);
731 message.push_str(" invalid api key");
732 let guided = VtCodeError::execution(ErrorCode::Unexpected, message).with_misconfiguration_guidance();
733 assert!(guided.message.contains("Check settings/config first"));
734 assert!(guided.to_string().contains("Correct the configuration before retrying."));
735 }
736}