use super::{BzOutcome, BzRunner};
use crate::cli_outbound::{Binary, Chunk, Cli, ExitInfo};
use crate::xdg::Env;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct RealBzRunner {
cli: Cli,
}
impl RealBzRunner {
pub fn new(cli: Cli) -> Self {
Self { cli }
}
pub fn resolve(world: &Env) -> Self {
Self::new(Cli::resolve_in_world(
Binary::Bz,
&crate::world::overrides(world),
))
}
fn collect(&self, args: &[&str]) -> BzOutcome {
match self.cli.run(args) {
Ok(stream) => {
let mut out = Vec::new();
let mut err = Vec::new();
let mut success = false;
for chunk in stream {
match chunk {
Chunk::Stdout(b) => out.extend(b),
Chunk::Stderr(b) => err.extend(b),
Chunk::Exited(e) => success = e == ExitInfo::Code(0),
}
}
BzOutcome {
success,
stdout: String::from_utf8_lossy(&out).into_owned(),
stderr: String::from_utf8_lossy(&err).into_owned(),
}
}
Err(e) => BzOutcome {
success: false,
stdout: String::new(),
stderr: e.to_string(),
},
}
}
}
impl BzRunner for RealBzRunner {
fn dump_config_at(&self, config: &Path) -> BzOutcome {
self.collect(&["--config", &config.display().to_string(), "--dump-config"])
}
fn dump_config_effective(&self) -> BzOutcome {
self.collect(&["--dump-config"])
}
fn list_models(&self, provider: &str) -> BzOutcome {
self.collect(&["--list-models", "--provider", provider, "--json"])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::spawn_guard;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tempfile::tempdir;
fn recorder(dir: &Path, log: &Path, code: i32) -> PathBuf {
let path = dir.join("bz");
let body = format!(
"#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {}; done\n\
printf 'OUT'\nprintf 'ERR' 1>&2\nexit {}\n",
log.display(),
code
);
fs::write(&path, body).unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap();
path
}
#[test]
fn resolve_uses_bz_binary_name() {
let world = Env::from_pairs([("HOME", "/h"), ("XDG_DATA_HOME", "/d")]);
assert_eq!(RealBzRunner::resolve(&world).cli.binary(), Path::new("bz"));
}
#[test]
fn dump_config_at_argv_and_success() {
let g = spawn_guard();
let dir = tempdir().unwrap();
let log = dir.path().join("argv");
let bin = recorder(dir.path(), &log, 0);
let runner = RealBzRunner::new(Cli::new(bin));
let out = runner.dump_config_at(Path::new("/x/temp.toml"));
drop(g);
assert_eq!(
fs::read_to_string(&log).unwrap(),
"--config\n/x/temp.toml\n--dump-config\n"
);
assert_eq!(
out,
BzOutcome {
success: true,
stdout: "OUT".into(),
stderr: "ERR".into(),
}
);
}
#[test]
fn dump_config_effective_argv() {
let g = spawn_guard();
let dir = tempdir().unwrap();
let log = dir.path().join("argv");
let bin = recorder(dir.path(), &log, 0);
let runner = RealBzRunner::new(Cli::new(bin));
let _ = runner.dump_config_effective();
drop(g);
assert_eq!(fs::read_to_string(&log).unwrap(), "--dump-config\n");
}
#[test]
fn list_models_argv_and_nonzero_is_failure() {
let g = spawn_guard();
let dir = tempdir().unwrap();
let log = dir.path().join("argv");
let bin = recorder(dir.path(), &log, 3);
let runner = RealBzRunner::new(Cli::new(bin));
let out = runner.list_models("openai");
drop(g);
assert_eq!(
fs::read_to_string(&log).unwrap(),
"--list-models\n--provider\nopenai\n--json\n"
);
assert!(!out.success);
assert_eq!(out.stderr, "ERR");
}
#[test]
fn spawn_failure_is_a_nonsuccess_outcome() {
let _g = spawn_guard();
let runner = RealBzRunner::new(Cli::new("/definitely/not/a/real/bz-xyz"));
let out = runner.dump_config_effective();
assert!(!out.success);
assert!(!out.stderr.is_empty());
}
}