use super::*;
#[test]
fn test_extracted_stdout_error_debug_log_is_gated_by_verbosity() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
"{\"type\":\"error\",\"error\":{\"code\":\"usage_limit_exceeded\"}}\n".to_string(),
));
let logger = Logger::new(colors).with_workspace_log(
Arc::clone(&workspace) as Arc<dyn Workspace>,
".agent/logs/pipeline.log",
);
let mut timer = Timer::new();
let config = Config::default().with_verbosity(crate::config::Verbosity::Normal);
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(1, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "opencode",
cmd_str: "claude -p",
parser_type: JsonParserType::OpenCode,
env_vars: &env_vars,
prompt: "hello",
display_name: "opencode",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let _ = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
let logs = workspace
.read(Path::new(".agent/logs/pipeline.log"))
.expect("pipeline log should be readable");
assert!(!logs.contains("[DEBUG] [OpenCode] Extracted error from logfile"));
}
#[test]
fn test_io_timeout_from_run_with_prompt_err_arm_returns_invocation_failed() {
let colors = Colors { enabled: false };
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let inner_ws = MemoryWorkspace::new_test();
let workspace = TimedOutWriteWorkspace::new(inner_ws, PathBuf::from(".agent/last_prompt.txt"));
let executor = Arc::new(crate::executor::MockProcessExecutor::new());
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: &workspace,
workspace_arc: std::sync::Arc::new(workspace.clone()),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
!matches!(
result.event,
PipelineEvent::Agent(AgentEvent::TimedOut { .. })
),
"I/O timeout from run_with_prompt Err arm must NOT emit TimedOut; got: {:?}",
result.event
);
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"I/O timeout from run_with_prompt Err arm must emit InvocationFailed; got: {:?}",
result.event
);
}
#[test]
fn test_classify_agent_error_sigsegv() {
let error_kind = classify_agent_error(139, "", None);
assert_eq!(error_kind, AgentErrorKind::InternalError);
}
#[test]
fn test_classify_agent_error_sigabrt() {
let error_kind = classify_agent_error(134, "", None);
assert_eq!(error_kind, AgentErrorKind::InternalError);
}
#[test]
fn test_classify_agent_error_sigterm() {
let error_kind = classify_agent_error(143, "", None);
assert_eq!(error_kind, AgentErrorKind::Timeout);
}
#[test]
fn test_classify_agent_error_timeout_from_stderr() {
let error_kind = classify_agent_error(1, "Connection timeout", None);
assert_eq!(error_kind, AgentErrorKind::Timeout);
}
#[test]
fn test_classify_agent_error_network_connection_reset() {
let error_kind = classify_agent_error(1, "Connection reset by peer", None);
assert_eq!(error_kind, AgentErrorKind::Network);
}
#[test]
fn test_classify_agent_error_rate_limit() {
let error_kind = classify_agent_error(1, "Rate limit exceeded", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_matches_http_429() {
let error_kind = classify_agent_error(1, "HTTP 429: Rate limit reached for requests", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_matches_bare_http_429() {
let error_kind = classify_agent_error(1, "HTTP 429", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_matches_bare_status_429() {
let error_kind = classify_agent_error(1, "status 429", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_overrides_auth_for_403_forbidden_rate_limit() {
let error_kind = classify_agent_error(1, "HTTP 403 Forbidden: rate limit exceeded", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_overrides_auth_for_403_forbidden_quota_exceeded() {
let error_kind =
classify_agent_error(1, "HTTP 403 Forbidden: exceeded your current quota", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_from_opencode_json_error() {
let stderr = r#"✗ Error: {"type":"error","sequence_number":2,"error":{"type":"tokens","code":"rate_limit_exceeded","message":"Rate limit reached"}}"#;
let error_kind = classify_agent_error(1, stderr, None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_does_not_treat_429_token_count_as_rate_limit() {
let error_kind = classify_agent_error(1, "Parse error: expected 429 tokens", None);
assert_eq!(error_kind, AgentErrorKind::ParsingError);
}
#[test]
fn test_classify_agent_error_does_not_treat_quota_word_as_rate_limit() {
let error_kind = classify_agent_error(1, "quota.rs:1:1: syntax error", None);
assert_ne!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_authentication() {
let error_kind = classify_agent_error(1, "Invalid API key", None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_classify_agent_error_model_unavailable() {
let error_kind = classify_agent_error(1, "Model not found", None);
assert_eq!(error_kind, AgentErrorKind::ModelUnavailable);
}
#[test]
fn test_is_retriable_agent_error() {
assert!(is_retriable_agent_error(&AgentErrorKind::Network));
assert!(is_retriable_agent_error(&AgentErrorKind::ModelUnavailable));
assert!(!is_retriable_agent_error(&AgentErrorKind::Timeout));
assert!(!is_retriable_agent_error(&AgentErrorKind::RateLimit));
assert!(!is_retriable_agent_error(&AgentErrorKind::Authentication));
assert!(!is_retriable_agent_error(&AgentErrorKind::ParsingError));
assert!(!is_retriable_agent_error(&AgentErrorKind::FileSystem));
assert!(!is_retriable_agent_error(&AgentErrorKind::InternalError));
}
#[test]
fn test_is_timeout_error() {
assert!(is_timeout_error(&AgentErrorKind::Timeout));
assert!(!is_timeout_error(&AgentErrorKind::Network));
assert!(!is_timeout_error(&AgentErrorKind::RateLimit));
assert!(!is_timeout_error(&AgentErrorKind::ModelUnavailable));
assert!(!is_timeout_error(&AgentErrorKind::Authentication));
assert!(!is_timeout_error(&AgentErrorKind::ParsingError));
assert!(!is_timeout_error(&AgentErrorKind::FileSystem));
assert!(!is_timeout_error(&AgentErrorKind::InternalError));
}
#[test]
fn test_is_rate_limit_error() {
assert!(is_rate_limit_error(&AgentErrorKind::RateLimit));
assert!(!is_rate_limit_error(&AgentErrorKind::Network));
assert!(!is_rate_limit_error(&AgentErrorKind::Timeout));
assert!(!is_rate_limit_error(&AgentErrorKind::ModelUnavailable));
assert!(!is_rate_limit_error(&AgentErrorKind::Authentication));
assert!(!is_rate_limit_error(&AgentErrorKind::ParsingError));
assert!(!is_rate_limit_error(&AgentErrorKind::FileSystem));
assert!(!is_rate_limit_error(&AgentErrorKind::InternalError));
}
#[test]
fn test_error_preview_truncates_on_char_boundary() {
let message = "Error 🚫: usage limit reached";
let preview = build_error_preview(message, 10);
assert!(message.starts_with(&preview));
assert!(preview.chars().count() <= 10);
}
#[test]
fn test_is_auth_error() {
assert!(is_auth_error(&AgentErrorKind::Authentication));
assert!(!is_auth_error(&AgentErrorKind::RateLimit));
assert!(!is_auth_error(&AgentErrorKind::Network));
assert!(!is_auth_error(&AgentErrorKind::Timeout));
assert!(!is_auth_error(&AgentErrorKind::ModelUnavailable));
assert!(!is_auth_error(&AgentErrorKind::ParsingError));
assert!(!is_auth_error(&AgentErrorKind::FileSystem));
assert!(!is_auth_error(&AgentErrorKind::InternalError));
}
#[test]
fn test_classify_agent_error_auth_401() {
let error_kind = classify_agent_error(1, "HTTP 401 Unauthorized", None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_classify_agent_error_auth_403_forbidden() {
let error_kind = classify_agent_error(1, "HTTP 403 Forbidden", None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_classify_agent_error_auth_invalid_token() {
let error_kind = classify_agent_error(1, "Error: Invalid token provided", None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_classify_agent_error_auth_credential() {
let error_kind = classify_agent_error(1, "Error: This credential is not authorized", None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_classify_agent_error_auth_access_denied() {
let error_kind = classify_agent_error(1, "Access denied: insufficient permissions", None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_classify_io_error_timeout() {
let error = io::Error::new(io::ErrorKind::TimedOut, "Operation timeout");
let error_kind = classify_io_error(&error);
assert_eq!(error_kind, AgentErrorKind::Timeout);
}
#[test]
fn test_classify_io_error_timeout_timed_out_message() {
let error = io::Error::new(io::ErrorKind::TimedOut, "Operation timed out");
let error_kind = classify_io_error(&error);
assert_eq!(error_kind, AgentErrorKind::Timeout);
}
#[test]
fn test_classify_io_error_filesystem() {
let error = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
let error_kind = classify_io_error(&error);
assert_eq!(error_kind, AgentErrorKind::FileSystem);
}
#[test]
fn test_classify_io_error_network() {
let error = io::Error::new(io::ErrorKind::BrokenPipe, "Broken pipe");
let error_kind = classify_io_error(&error);
assert_eq!(error_kind, AgentErrorKind::Network);
}
#[test]
fn test_timeout_with_empty_logfile_emits_no_output() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
String::new(), ));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_nonempty_logfile_emits_partial_output() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
"Some partial output\n".to_string(), ));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_missing_logfile_defaults_to_no_output() {
let colors = Colors { enabled: false };
let workspace = ReadFailWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
);
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::new(workspace.clone()) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: &workspace,
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_9_non_whitespace_chars_emits_no_output() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
"123456789".to_string(),
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_10_non_whitespace_chars_emits_partial_output() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
"1234567890".to_string(),
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_whitespace_only_logfile_emits_no_output() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
" \n\t\n ".to_string(),
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_meaningful_output_surrounded_by_whitespace() {
let colors = Colors { enabled: false };
let workspace = Arc::new(ReadHijackWorkspace::new(
MemoryWorkspace::new_test(),
PathBuf::from(".agent/logs/test.log"),
" hello world \n\n".to_string(),
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: None,
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_classify_agent_error_rate_limit_quota_exceeded() {
let error_kind = classify_agent_error(1, "API quota exceeded, please try again later", None);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_classify_agent_error_rate_limit_anthropic_quota() {
let error_kind = classify_agent_error(
1,
"You have exceeded your current quota for this API tier",
None,
);
assert_eq!(error_kind, AgentErrorKind::RateLimit);
}
#[test]
fn test_auth_error_triggers_auth_fallback_classification() {
let auth_patterns = vec![
"HTTP 401 Unauthorized",
"HTTP 403 Forbidden",
"Error: Invalid API key",
"Error: Invalid token provided",
"Access denied: insufficient permissions",
"This credential is only authorized for use with Claude Code",
"Authentication failed: bad credentials",
];
for pattern in auth_patterns {
let error_kind = classify_agent_error(1, pattern, None);
assert_eq!(
error_kind,
AgentErrorKind::Authentication,
"Pattern '{pattern}' should classify as Authentication"
);
assert!(
is_auth_error(&error_kind),
"Authentication error kind should trigger auth fallback for pattern '{pattern}'"
);
}
}
#[test]
fn test_rate_limit_error_triggers_rate_limit_fallback_classification() {
let rate_limit_patterns = vec![
"Rate limit exceeded",
"Rate limit reached for requests",
"HTTP 429 Too Many Requests",
"Error: too many requests, please slow down",
"exceeded your current quota",
"API quota exceeded",
];
for pattern in rate_limit_patterns {
let error_kind = classify_agent_error(1, pattern, None);
assert_eq!(
error_kind,
AgentErrorKind::RateLimit,
"Pattern '{pattern}' should classify as RateLimit"
);
assert!(
is_rate_limit_error(&error_kind),
"RateLimit error kind should trigger rate limit fallback for pattern '{pattern}'"
);
}
}
#[test]
fn test_classify_agent_error_auth_from_json_error() {
let stderr = r#"✗ Error: {"type":"error","error":{"type":"auth","code":"unauthorized","message":"Invalid API key provided"}}"#;
let error_kind = classify_agent_error(1, stderr, None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_timeout_with_valid_completion_file_emits_success() {
use crate::reducer::event::AgentEvent;
let colors = Colors { enabled: false };
let completion_path = std::path::Path::new(".agent/tmp/development_result.xml");
let workspace = Arc::new(MemoryWorkspace::new_test().with_file(
".agent/tmp/development_result.xml",
"<ralph-development-result><status>completed</status></ralph-development-result>",
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: Some(completion_path),
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
),
"SIGTERM + valid completion file should emit InvocationSucceeded, got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_missing_completion_file_emits_no_result() {
let colors = Colors { enabled: false };
let completion_path = std::path::Path::new(".agent/tmp/development_result.xml");
let workspace = Arc::new(MemoryWorkspace::new_test());
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test.log",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: Some(completion_path),
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_timeout_with_invalid_completion_file_emits_partial_result() {
let colors = Colors { enabled: false };
let completion_path = std::path::Path::new(".agent/tmp/development_result.xml");
let workspace = Arc::new(MemoryWorkspace::new_test().with_file(
".agent/tmp/development_result.xml",
"truncated non-xml content",
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(143, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test.log",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: Some(completion_path),
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"SIGTERM without timeout_context must return InvocationFailed; got {:?}",
result.event
);
}
#[test]
fn test_non_sigterm_exit_with_valid_result_emits_success() {
use crate::reducer::event::AgentEvent;
let colors = Colors { enabled: false };
let completion_path = std::path::Path::new(".agent/tmp/development_result.xml");
let workspace = Arc::new(MemoryWorkspace::new_test().with_file(
".agent/tmp/development_result.xml",
"<ralph-development-result><status>completed</status></ralph-development-result>",
));
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(91, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: Some(completion_path),
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
),
"exit code 91 + valid completion file must emit InvocationSucceeded, got {:?}",
result.event
);
}
#[test]
fn test_non_sigterm_exit_without_result_emits_failure() {
use crate::reducer::event::AgentEvent;
let colors = Colors { enabled: false };
let completion_path = std::path::Path::new(".agent/tmp/development_result.xml");
let workspace = Arc::new(MemoryWorkspace::new_test());
let logger = Logger::new(colors);
let mut timer = Timer::new();
let config = Config::default();
let executor = Arc::new(
crate::executor::MockProcessExecutor::new().with_agent_result(
"claude",
Ok(crate::executor::AgentCommandResult::failure(91, "")),
),
);
let executor_arc: Arc<dyn crate::executor::ProcessExecutor> = executor;
let workspace_arc = Arc::clone(&workspace) as Arc<dyn crate::workspace::Workspace>;
let mut runtime = PipelineRuntime {
timer: &mut timer,
logger: &logger,
colors: &colors,
config: &config,
executor: executor_arc.as_ref(),
executor_arc: Arc::clone(&executor_arc),
workspace: workspace.as_ref(),
workspace_arc: Arc::clone(&workspace_arc),
};
let env_vars: HashMap<String, String> = HashMap::new();
let exec_config = AgentExecutionConfig {
role: AgentRole::Developer,
agent_name: "claude",
cmd_str: "claude -p",
parser_type: JsonParserType::Claude,
env_vars: &env_vars,
prompt: "hello",
display_name: "claude",
log_prefix: ".agent/logs/test",
model_index: 0,
attempt: 0,
logfile: ".agent/logs/test.log",
completion_output_path: Some(completion_path),
};
let result = execute_agent_fault_tolerantly(exec_config, &mut runtime)
.expect("executor should never return Err");
assert!(
matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationFailed { .. })
),
"exit code 91 + missing completion file must emit InvocationFailed, got {:?}",
result.event
);
}
#[test]
fn test_classify_agent_error_403_from_json_error() {
let stderr = r#"{"error":{"code":"403","message":"Forbidden: API key does not have access"}}"#;
let error_kind = classify_agent_error(1, stderr, None);
assert_eq!(error_kind, AgentErrorKind::Authentication);
}
#[test]
fn test_non_special_errors_maintain_retry_semantics() {
let network_error = classify_agent_error(1, "Connection refused", None);
assert_eq!(network_error, AgentErrorKind::Network);
assert!(
is_retriable_agent_error(&network_error),
"Network should be retriable"
);
assert!(
!is_rate_limit_error(&network_error),
"Network should not trigger rate limit fallback"
);
assert!(
!is_auth_error(&network_error),
"Network should not trigger auth fallback"
);
let connection_timeout = classify_agent_error(1, "Connection timeout", None);
assert_eq!(connection_timeout, AgentErrorKind::Timeout);
assert!(!is_retriable_agent_error(&connection_timeout));
assert!(is_timeout_error(&connection_timeout));
let timeout_error = classify_agent_error(143, "", None); assert_eq!(timeout_error, AgentErrorKind::Timeout);
assert!(!is_retriable_agent_error(&timeout_error));
assert!(is_timeout_error(&timeout_error));
let model_error = classify_agent_error(1, "Model not found", None);
assert_eq!(model_error, AgentErrorKind::ModelUnavailable);
assert!(is_retriable_agent_error(&model_error));
let internal_error = classify_agent_error(139, "", None); assert_eq!(internal_error, AgentErrorKind::InternalError);
assert!(!is_retriable_agent_error(&internal_error));
let parse_error = classify_agent_error(1, "Parse error: invalid syntax", None);
assert_eq!(parse_error, AgentErrorKind::ParsingError);
assert!(!is_retriable_agent_error(&parse_error));
let fs_error = classify_agent_error(1, "Permission denied: /tmp/foo", None);
assert_eq!(fs_error, AgentErrorKind::FileSystem);
assert!(!is_retriable_agent_error(&fs_error));
}