use std::env;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentResolutionError {
pub message: String,
pub code: &'static str,
}
impl std::fmt::Display for AgentResolutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for AgentResolutionError {}
trait EnvReader {
fn get(&self, key: &str) -> Option<String>;
fn is_tty(&self) -> bool;
}
struct RealEnv;
impl EnvReader for RealEnv {
fn get(&self, key: &str) -> Option<String> {
env::var(key).ok().filter(|v| !v.is_empty())
}
fn is_tty(&self) -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal()
}
}
fn resolve_agent_with(cli_flag: Option<&str>, env: &dyn EnvReader) -> Option<String> {
if let Some(agent) = cli_flag
&& !agent.is_empty()
{
return Some(agent.to_string());
}
if let Some(val) = env.get("BONES_AGENT") {
return Some(val);
}
if let Some(val) = env.get("AGENT") {
return Some(val);
}
if env.is_tty()
&& let Some(val) = env.get("USER")
{
return Some(val);
}
None
}
pub fn resolve_agent(cli_flag: Option<&str>) -> Option<String> {
resolve_agent_with(cli_flag, &RealEnv)
}
pub fn require_agent(cli_flag: Option<&str>) -> Result<String, AgentResolutionError> {
resolve_agent(cli_flag).ok_or_else(|| AgentResolutionError {
message: "Agent identity required for this command. \
Set --agent, BONES_AGENT, or AGENT environment variable."
.to_string(),
code: "missing_agent",
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct MockEnv {
vars: HashMap<String, String>,
tty: bool,
}
impl MockEnv {
fn new() -> Self {
Self {
vars: HashMap::new(),
tty: false,
}
}
fn var(mut self, key: &str, val: &str) -> Self {
self.vars.insert(key.to_string(), val.to_string());
self
}
fn tty(mut self) -> Self {
self.tty = true;
self
}
}
impl EnvReader for MockEnv {
fn get(&self, key: &str) -> Option<String> {
self.vars.get(key).filter(|v| !v.is_empty()).cloned()
}
fn is_tty(&self) -> bool {
self.tty
}
}
#[test]
fn cli_flag_takes_priority() {
let env = MockEnv::new()
.var("BONES_AGENT", "env-bones")
.var("AGENT", "env-agent");
let result = resolve_agent_with(Some("flag-agent"), &env);
assert_eq!(result.as_deref(), Some("flag-agent"));
}
#[test]
fn bones_agent_env_fallback() {
let env = MockEnv::new()
.var("BONES_AGENT", "env-bones")
.var("AGENT", "env-agent");
let result = resolve_agent_with(None, &env);
assert_eq!(result.as_deref(), Some("env-bones"));
}
#[test]
fn agent_env_fallback() {
let env = MockEnv::new().var("AGENT", "env-agent");
let result = resolve_agent_with(None, &env);
assert_eq!(result.as_deref(), Some("env-agent"));
}
#[test]
fn empty_flag_ignored() {
let env = MockEnv::new().var("BONES_AGENT", "env-bones");
let result = resolve_agent_with(Some(""), &env);
assert_eq!(result.as_deref(), Some("env-bones"));
}
#[test]
fn empty_env_ignored() {
let env = MockEnv::new()
.var("BONES_AGENT", "")
.var("AGENT", "real-agent");
let result = resolve_agent_with(None, &env);
assert_eq!(result.as_deref(), Some("real-agent"));
}
#[test]
fn user_env_only_in_tty() {
let env = MockEnv::new().var("USER", "bob");
let result = resolve_agent_with(None, &env);
assert_eq!(result, None);
let env = MockEnv::new().var("USER", "bob").tty();
let result = resolve_agent_with(None, &env);
assert_eq!(result.as_deref(), Some("bob"));
}
#[test]
fn no_identity_returns_none() {
let env = MockEnv::new();
let result = resolve_agent_with(None, &env);
assert_eq!(result, None);
}
#[test]
fn resolution_chain_order() {
let env = MockEnv::new()
.var("BONES_AGENT", "bones")
.var("AGENT", "agent")
.var("USER", "user")
.tty();
assert_eq!(
resolve_agent_with(Some("flag"), &env).as_deref(),
Some("flag")
);
assert_eq!(resolve_agent_with(None, &env).as_deref(), Some("bones"));
let env = MockEnv::new()
.var("AGENT", "agent")
.var("USER", "user")
.tty();
assert_eq!(resolve_agent_with(None, &env).as_deref(), Some("agent"));
let env = MockEnv::new().var("USER", "user").tty();
assert_eq!(resolve_agent_with(None, &env).as_deref(), Some("user"));
}
#[test]
fn require_agent_returns_error_when_missing() {
let result = require_agent(None);
let err = AgentResolutionError {
message: "test".to_string(),
code: "missing_agent",
};
assert_eq!(err.code, "missing_agent");
assert_eq!(format!("{err}"), "test");
let _: Box<dyn std::error::Error> = Box::new(err);
drop(result); }
#[test]
fn require_agent_succeeds_with_flag() {
let result = require_agent(Some("test-agent"));
assert_eq!(result.unwrap(), "test-agent");
}
}