use crate::brain::bash_failure::{BashFailureKind, classify, exit_code, stderr_head};
fn snippet(code: i32, stdout: &str, stderr: &str) -> String {
let mut captured = String::new();
if !stdout.is_empty() {
captured.push_str(&format!("STDOUT:\n{stdout}"));
}
if !stderr.is_empty() {
if !captured.is_empty() {
captured.push_str("\n\n");
}
captured.push_str(&format!("STDERR:\n{stderr}"));
}
format!("Command exited with code {code}\n\n-- output captured before error --\n{captured}")
}
#[test]
fn a_missing_file_is_environmental() {
let s = snippet(1, "", "cat: /etc/nope.conf: No such file or directory\n");
assert_eq!(classify(&s), BashFailureKind::Environmental);
}
#[test]
fn a_dead_service_is_environmental() {
for stderr in [
"curl: (7) Failed to connect to localhost port 8931: Connection refused\n",
"ssh: connect to host 10.0.0.9 port 22: Operation timed out\n",
"curl: (6) Could not resolve host: example.invalid\n",
"psql: error: connection to server failed: Permission denied\n",
] {
assert_eq!(
classify(&snippet(1, "", stderr)),
BashFailureKind::Environmental,
"must be environmental: {stderr}"
);
}
}
#[test]
fn a_command_the_model_got_wrong_stays_a_defect() {
for stderr in [
"bash: line 3: syntax error near unexpected token `fi'\n",
"bash: frobnicate: command not found\n",
"bash: line 1: MY_VAR: unbound variable\n",
"ls: illegal option -- Z\n",
] {
assert_eq!(
classify(&snippet(2, "", stderr)),
BashFailureKind::ModelError,
"must stay a defect: {stderr}"
);
}
}
#[test]
fn a_model_error_wins_over_an_environmental_phrase() {
let s = snippet(
2,
"",
"cat: /tmp/nope: No such file or directory\nbash: syntax error near unexpected token `done'\n",
);
assert_eq!(classify(&s), BashFailureKind::ModelError);
}
#[test]
fn an_unrecognised_failure_stays_a_defect() {
let s = snippet(1, "", "error: the sky is the wrong colour\n");
assert_eq!(classify(&s), BashFailureKind::Unknown);
}
#[test]
fn stdout_content_cannot_exempt_a_failure() {
let s = snippet(
1,
"2026-08-16 connection refused\n2026-08-16 no such file or directory\n",
"error: the sky is the wrong colour\n",
);
assert_eq!(classify(&s), BashFailureKind::Unknown);
}
#[test]
fn a_snippet_with_no_stderr_section_is_still_read() {
let s = snippet(1, "fatal: destination path already exists\n", "");
assert_eq!(classify(&s), BashFailureKind::Unknown);
let s = snippet(127, "bash: nope: command not found\n", "");
assert_eq!(classify(&s), BashFailureKind::ModelError);
}
#[test]
fn the_exit_code_is_read_off_the_error_line() {
assert_eq!(exit_code(&snippet(127, "", "x")), Some(127));
assert_eq!(exit_code(&snippet(1, "", "x")), Some(1));
assert_eq!(exit_code("no exit line here"), None);
}
#[test]
fn the_stderr_head_is_the_diagnostic_not_the_stdout() {
let s = snippet(
1,
"lots of unrelated stdout\n",
"fatal: not a git repository\n",
);
assert_eq!(stderr_head(&s, 160), Some("fatal: not a git repository"));
assert_eq!(stderr_head(&snippet(1, "out", ""), 160), None);
assert_eq!(stderr_head(&snippet(1, "", " \n"), 160), None);
}
#[test]
fn a_long_stderr_head_is_capped_on_a_char_boundary() {
let s = snippet(1, "", &"é".repeat(300));
let head = stderr_head(&s, 10).expect("stderr present");
assert_eq!(head.chars().count(), 10);
}