diff --git a/src/config.rs b/src/config.rs index 421406d..358fec6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -153,8 +153,14 @@ pub enum ConfigError { impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ConfigError::ReadError(e) => write!(f, "failed to read bugatti.config.toml: {e}"), - ConfigError::ParseError(e) => write!(f, "invalid bugatti.config.toml: {e}"), + ConfigError::ReadError(e) => write!( + f, + "failed to read bugatti.config.toml: {e}. Check that the file exists and is readable." + ), + ConfigError::ParseError(e) => write!( + f, + "invalid bugatti.config.toml: {e}. See https://bugatti.dev/llms/cli-reference.txt for config format." + ), } } } @@ -288,6 +294,18 @@ cmd = "echo migrate" assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("invalid bugatti.config.toml")); + assert!(err_msg.contains("https://bugatti.dev/llms/cli-reference.txt")); + } + + #[test] + fn read_error_includes_actionable_hint() { + let err_msg = ConfigError::ReadError(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "permission denied", + )) + .to_string(); + assert!(err_msg.contains("failed to read bugatti.config.toml")); + assert!(err_msg.contains("Check that the file exists and is readable")); } #[test] diff --git a/src/main.rs b/src/main.rs index ced57bc..72700f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use bugatti::exit_code::{ EXIT_STEP_ERROR, }; use bugatti::expand; -use bugatti::provider::AgentSession; +use bugatti::provider::{AgentSession, ProviderError}; use bugatti::report::{self, ReportInput}; use bugatti::run::{self, ArtifactDir, EffectiveConfigSummary}; use bugatti::test_file; @@ -35,6 +35,36 @@ fn relative_display(path: &Path) -> String { .unwrap_or_else(|| path.display().to_string()) } +/// Build a user-facing error for a missing test file. +fn test_file_not_found_message(input: &str) -> String { + format!( + "ERROR: test file not found: {input}. Tip: run `bugatti test` to discover available tests." + ) +} + +/// Message shown when discovery finds no runnable root test files. +fn no_root_tests_found_message() -> &'static str { + "No root test files found. Create a .test.toml file - see https://bugatti.dev/getting-started" +} + +/// Build a provider-initialization error with actionable guidance for common setup misses. +fn provider_initialization_error_message(err: &ProviderError) -> String { + match err { + ProviderError::InitializationFailed(inner) => { + if inner.contains("claude CLI binary not found in PATH") + && !inner.contains("Install Claude Code:") + { + format!( + "provider initialization failed: {inner}. Install Claude Code: https://docs.anthropic.com/en/docs/claude-code" + ) + } else { + err.to_string() + } + } + _ => err.to_string(), + } +} + /// Check whether the run has been interrupted by Ctrl+C. pub fn is_interrupted() -> bool { INTERRUPTED.load(Ordering::Relaxed) @@ -108,7 +138,7 @@ fn main() { Some(p) => { let test_path = PathBuf::from(&p); if !test_path.exists() { - eprintln!("ERROR: test file not found: {p}"); + eprintln!("{}", test_file_not_found_message(&p)); EXIT_CONFIG_ERROR } else { let result = run_test_pipeline( @@ -482,7 +512,7 @@ fn run_test_with_artifacts( tracing::error!(error = %e, "provider initialization failed"); return ctx.fail_early( EXIT_PROVIDER_ERROR, - format!("provider initialization failed: {e}"), + provider_initialization_error_message(&e), &mut tracked_processes, ); } @@ -615,7 +645,7 @@ fn run_discovery( if discovery.tests.is_empty() { if discovery.errors.is_empty() { - println!("No root test files found."); + println!("{}", no_root_tests_found_message()); return EXIT_OK; } else { eprintln!( @@ -791,6 +821,12 @@ mod tests { use clap::Parser; use bugatti::cli::Cli; + use bugatti::provider::ProviderError; + + use crate::{ + no_root_tests_found_message, provider_initialization_error_message, + test_file_not_found_message, + }; #[test] fn test_subcommand_no_path() { @@ -947,4 +983,45 @@ mod tests { Ok(_) => panic!("--help should produce an error-like result from clap"), } } + + #[test] + fn test_test_file_not_found_message_includes_tip() { + let msg = test_file_not_found_message("ftue"); + assert!(msg.contains("test file not found: ftue")); + assert!(msg.contains("run `bugatti test` to discover available tests")); + } + + #[test] + fn test_no_root_tests_found_message_includes_getting_started_link() { + let msg = no_root_tests_found_message(); + assert!(msg.contains("No root test files found")); + assert!(msg.contains("https://bugatti.dev/getting-started")); + } + + #[test] + fn test_provider_initialization_error_message_adds_claude_install_hint() { + let err = ProviderError::InitializationFailed( + "claude CLI binary not found in PATH: No such file or directory".to_string(), + ); + let msg = provider_initialization_error_message(&err); + assert!(msg.contains("claude CLI binary not found in PATH")); + assert!(msg.contains("Install Claude Code: https://docs.anthropic.com/en/docs/claude-code")); + } + + #[test] + fn test_provider_initialization_error_message_leaves_other_errors_unchanged() { + let err = ProviderError::InitializationFailed("some other init error".to_string()); + let msg = provider_initialization_error_message(&err); + assert_eq!(msg, "provider initialization failed: some other init error"); + } + + #[test] + fn test_provider_initialization_error_message_does_not_duplicate_install_hint() { + let err = ProviderError::InitializationFailed( + "claude CLI binary not found in PATH: No such file or directory. Install Claude Code: https://docs.anthropic.com/en/docs/claude-code" + .to_string(), + ); + let msg = provider_initialization_error_message(&err); + assert_eq!(msg, err.to_string()); + } }