Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
),
}
}
}
Expand Down Expand Up @@ -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]
Expand Down
85 changes: 81 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
);
}
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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());
}
}