use crate::domain::agent::AgentKind;
pub(crate) fn format_agent_cli_exit_error(
agent_kind: AgentKind,
command_label: &str,
exit_code: Option<i32>,
stdout: &str,
stderr: &str,
) -> String {
if let Some(guidance) = known_agent_cli_exit_guidance(agent_kind, command_label, stdout, stderr)
{
return guidance;
}
let exit_code = exit_code.map_or_else(|| "unknown".to_string(), |code| code.to_string());
let output_detail = agent_cli_output_detail(stdout, stderr);
format!("{command_label} failed with exit code {exit_code}: {output_detail}")
}
fn known_agent_cli_exit_guidance(
agent_kind: AgentKind,
command_label: &str,
stdout: &str,
stderr: &str,
) -> Option<String> {
match agent_kind {
AgentKind::Antigravity if is_antigravity_authentication_error(stdout, stderr) => {
Some(antigravity_authentication_error_message(command_label))
}
AgentKind::Claude if is_claude_authentication_error(stdout, stderr) => {
Some(claude_authentication_error_message(command_label))
}
AgentKind::Antigravity | AgentKind::Claude | AgentKind::Codex | AgentKind::Gemini => None,
}
}
fn antigravity_authentication_error_message(command_label: &str) -> String {
format!(
"{command_label} failed because Antigravity authentication is missing.\nRun `agy` in a \
terminal to complete Google sign-in, then retry."
)
}
fn claude_authentication_error_message(command_label: &str) -> String {
format!(
"{command_label} failed because Claude authentication expired or is missing.\nRun `claude \
auth login` to refresh your Anthropic session, verify with `claude auth status`, then \
retry."
)
}
fn is_antigravity_authentication_error(stdout: &str, stderr: &str) -> bool {
let combined_output = format!("{stdout}\n{stderr}").to_ascii_lowercase();
combined_output.contains("you are not logged into antigravity")
|| combined_output.contains("failed to get oauth token")
|| combined_output.contains("error getting token source")
}
fn is_claude_authentication_error(stdout: &str, stderr: &str) -> bool {
let combined_output = format!("{stdout}\n{stderr}").to_ascii_lowercase();
combined_output.contains("oauth token has expired")
|| combined_output.contains("failed to authenticate")
|| combined_output.contains("authentication_error")
}
fn agent_cli_output_detail(stdout: &str, stderr: &str) -> String {
let trimmed_stdout = stdout.trim();
let trimmed_stderr = stderr.trim();
match (trimmed_stdout.is_empty(), trimmed_stderr.is_empty()) {
(false, false) => format!("stdout: {trimmed_stdout}; stderr: {trimmed_stderr}"),
(false, true) => format!("stdout: {trimmed_stdout}"),
(true, false) => format!("stderr: {trimmed_stderr}"),
(true, true) => "no output".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_generic_error_with_exit_code_and_stderr() {
let result = format_agent_cli_exit_error(
AgentKind::Claude,
"claude code",
Some(1),
"",
"something broke",
);
assert_eq!(
result,
"claude code failed with exit code 1: stderr: something broke"
);
}
#[test]
fn test_format_generic_error_with_unknown_exit_code() {
let result =
format_agent_cli_exit_error(AgentKind::Gemini, "gemini", None, "", "crash output");
assert!(result.contains("exit code unknown"));
}
#[test]
fn test_format_generic_error_with_both_stdout_and_stderr() {
let result =
format_agent_cli_exit_error(AgentKind::Codex, "codex", Some(2), "out text", "err text");
assert!(result.contains("stdout: out text"));
assert!(result.contains("stderr: err text"));
}
#[test]
fn test_format_generic_error_with_no_output() {
let result = format_agent_cli_exit_error(AgentKind::Claude, "claude", Some(1), "", "");
assert!(result.contains("no output"));
}
#[test]
fn test_claude_auth_error_from_expired_oauth_token_in_stderr() {
let result = format_agent_cli_exit_error(
AgentKind::Claude,
"claude code",
Some(1),
"",
"OAuth token has expired",
);
assert!(result.contains("authentication expired or is missing"));
assert!(result.contains("claude auth login"));
}
#[test]
fn test_antigravity_auth_error_from_not_logged_in_message() {
let result = format_agent_cli_exit_error(
AgentKind::Antigravity,
"agy",
Some(1),
"",
"You are not logged into Antigravity.",
);
assert!(result.contains("Antigravity authentication is missing"));
assert!(result.contains("Run `agy`"));
}
#[test]
fn test_claude_auth_error_from_failed_to_authenticate_in_stdout() {
let result = format_agent_cli_exit_error(
AgentKind::Claude,
"claude code",
Some(1),
"Failed to authenticate user",
"",
);
assert!(result.contains("authentication expired or is missing"));
}
#[test]
fn test_claude_auth_error_from_authentication_error_keyword() {
let result = format_agent_cli_exit_error(
AgentKind::Claude,
"claude code",
Some(1),
"",
"authentication_error: invalid key",
);
assert!(result.contains("claude auth login"));
}
#[test]
fn test_claude_auth_detection_is_case_insensitive() {
let result = format_agent_cli_exit_error(
AgentKind::Claude,
"claude",
Some(1),
"OAUTH TOKEN HAS EXPIRED",
"",
);
assert!(result.contains("authentication expired or is missing"));
}
#[test]
fn test_non_claude_provider_ignores_auth_keywords() {
let result = format_agent_cli_exit_error(
AgentKind::Gemini,
"gemini",
Some(1),
"",
"OAuth token has expired",
);
assert!(!result.contains("claude auth login"));
assert!(result.contains("exit code 1"));
}
#[test]
fn test_output_detail_with_only_stdout() {
let detail = agent_cli_output_detail("output text", "");
assert_eq!(detail, "stdout: output text");
}
#[test]
fn test_output_detail_with_only_stderr() {
let detail = agent_cli_output_detail("", "error text");
assert_eq!(detail, "stderr: error text");
}
#[test]
fn test_output_detail_with_both_streams() {
let detail = agent_cli_output_detail("out", "err");
assert_eq!(detail, "stdout: out; stderr: err");
}
#[test]
fn test_output_detail_with_empty_streams() {
let detail = agent_cli_output_detail("", "");
assert_eq!(detail, "no output");
}
#[test]
fn test_output_detail_trims_whitespace() {
let detail = agent_cli_output_detail(" out ", " err ");
assert_eq!(detail, "stdout: out; stderr: err");
}
#[test]
fn test_is_claude_auth_error_returns_false_for_unrelated_output() {
assert!(!is_claude_authentication_error(
"normal output",
"normal error"
));
}
#[test]
fn test_is_claude_auth_error_returns_true_for_mixed_case_in_stderr() {
assert!(is_claude_authentication_error("", "FAILED TO AUTHENTICATE"));
}
#[test]
fn test_known_guidance_returns_none_for_non_auth_claude_error() {
let result =
known_agent_cli_exit_guidance(AgentKind::Claude, "claude", "normal out", "normal err");
assert!(result.is_none());
}
#[test]
fn test_known_guidance_returns_none_for_codex() {
let result =
known_agent_cli_exit_guidance(AgentKind::Codex, "codex", "OAuth token has expired", "");
assert!(result.is_none());
}
}