use super::fixtures::*;
use crate::prompt::Error;
use brazen::ErrorKind;
use serde_json::Value;
use std::io;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn run_with_harness(
repo: &Path,
msg: &str,
adapter: &StubAdapter,
git: &StubGit,
config_root: &Path,
) -> Result<String, Error> {
let clock = FixedClock::default();
let id = FixedIdGen;
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
crate::prompt::run(
repo,
msg,
&valid_deps(
adapter,
&sleeper,
git,
&clock,
&id,
&tool_executor,
config_root,
),
)
}
#[test]
fn run_surfaces_global_models_yaml_load_error() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let empty_harness = TempDir::new().unwrap();
let err = run_with_harness(
repo.path(),
"hi",
&unreachable_adapter(),
&StubGit::ok(),
empty_harness.path(),
)
.unwrap_err();
assert!(matches!(err, Error::Config(_)), "got {err:?}");
}
#[test]
fn run_surfaces_cross_check_failure() {
let bad_per_repo = r#"
roles:
worker:
provider: anthropic
model: nonexistent-model
"#;
let repo = scaffold_repo(bad_per_repo, Some("body"));
let err =
run_with_stubs(repo.path(), "hi", &unreachable_adapter(), &StubGit::ok()).unwrap_err();
assert!(matches!(err, Error::Config(_)));
}
#[test]
fn run_rejects_when_worker_role_missing() {
let no_worker = r#"
roles:
compactor:
provider: anthropic
model: claude-sonnet-5
"#;
let repo = scaffold_repo(no_worker, Some("body"));
let err =
run_with_stubs(repo.path(), "hi", &unreachable_adapter(), &StubGit::ok()).unwrap_err();
match err {
Error::RoleMissing(role) => assert_eq!(role, "worker"),
other => panic!("expected RoleMissing, got {other:?}"),
}
}
#[test]
fn run_surfaces_version_skew() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(b"bz 9.9.9\n")]);
let git = StubGit::ok();
let err = run_with_stubs(repo.path(), "hi", &adapter, &git).unwrap_err();
match err {
Error::VersionSkew { found, .. } => assert_eq!(found, "9.9.9"),
other => panic!("expected VersionSkew, got {other:?}"),
}
assert!(
git.runs.borrow().iter().all(|(_, a)| a[0] != "worktree"),
"no branch work before the guard passes"
);
}
#[test]
fn run_names_a_missing_adapter_and_the_command_that_installs_it() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let adapter = StubAdapter::scripted([StubAdapter::reply_err(io::ErrorKind::NotFound, "no bz")]);
let git = StubGit::ok();
let err = run_with_stubs(repo.path(), "hi", &adapter, &git).unwrap_err();
assert!(matches!(err, Error::AdapterMissing { .. }), "{err}");
let s = err.to_string();
assert!(
s.starts_with("provider adapter \"bz\" not found (ยง4.4 โ"),
"{s}"
);
assert!(
s.contains(&format!(
"cargo install brazen --version ={} --locked",
crate::prompt::brazen_pin()
)),
"{s}"
);
assert!(s.ends_with("): no bz"), "the errno trails as detail: {s}");
assert!(git.runs.borrow().iter().all(|(_, a)| a[0] != "worktree"));
}
#[test]
fn run_surfaces_model_call_spawn_failure() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let adapter = StubAdapter::scripted([
StubAdapter::reply_ok(&version_line()),
StubAdapter::reply_err(io::ErrorKind::BrokenPipe, "model call crashed"),
]);
let err = run_with_stubs(repo.path(), "hi", &adapter, &StubGit::ok()).unwrap_err();
assert!(matches!(err, Error::AdapterSpawn(_)));
}
#[test]
fn run_retries_on_retryable_error_then_completes() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let adapter = StubAdapter::scripted([
StubAdapter::reply_ok(&version_line()),
StubAdapter::reply_ok(&error_stream(
ErrorKind::Provider { status: 529 },
"overloaded",
)),
StubAdapter::reply_ok(&happy_response_bytes()),
]);
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
crate::prompt::run(
repo.path(),
"hi",
&valid_deps(
&adapter,
&sleeper,
&git,
&clock,
&id,
&tool_executor,
harness.path(),
),
)
.unwrap();
assert_eq!(sleeper.slept.borrow().len(), 1);
let resp = std::fs::read(repo.path().join("steps/ct-1-deadbeef/001/response.json")).unwrap();
let ends = resp
.split(|b| *b == b'\n')
.filter(|l| *l == br#"{"type":"end"}"#)
.count();
assert_eq!(ends, 2);
}
#[test]
fn run_surfaces_adapter_returning_in_band_error() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let adapter = StubAdapter::happy(&error_stream(ErrorKind::Auth, "unauthorized"));
let err = run_with_stubs(repo.path(), "hi", &adapter, &StubGit::ok()).unwrap_err();
match err {
Error::AdapterError { kind, message } => {
assert_eq!(kind, "Auth");
assert_eq!(message, "unauthorized");
}
other => panic!("expected AdapterError, got {other:?}"),
}
}
#[test]
fn error_display_includes_context() {
let _: String = Error::RoleMissing("worker".into()).to_string();
let _: String = Error::AdapterError {
kind: "Auth".into(),
message: "m".into(),
}
.to_string();
let _: String = Error::AdapterHalfStream {
stderr_log: "steps/c/001/stderr.log".into(),
tail: "(empty)".into(),
}
.to_string();
let _: String = Error::VersionSkew {
found: "9.9.9".into(),
expected: "0.0.2".into(),
}
.to_string();
let _: String = Error::HandshakeMismatch {
found: Some(2),
expected: 1,
}
.to_string();
let _: String = Error::ControlRead {
path: PathBuf::from("/x"),
source: io::Error::other("y"),
}
.to_string();
let _: String = Error::AdapterSpawn(io::Error::other("x")).to_string();
let _: String = Error::AdapterJson(serde_json::from_str::<Value>("{").unwrap_err()).to_string();
let _: String = Error::Git {
op: "add",
source: io::Error::other("x"),
}
.to_string();
let _: String = Error::Io(io::Error::other("x")).to_string();
let _: String = Error::ToolExec {
tool: "bash".into(),
source: crate::prompt::ExecError::KilledBySignal {
name: "bash".into(),
signal: 11,
},
}
.to_string();
let load_err: crate::config::LoadError = crate::config::LoadError::UnresolvedRef {
key: "k".into(),
message: "m".into(),
};
let e: Error = load_err.into();
let _: String = e.to_string();
}