pub mod hook;
use crate::config::AtmConfig;
use crate::error::AtmError;
use crate::types::AgentName;
pub(crate) fn resolve_actor_identity(
actor_override: Option<&str>,
config: Option<&AtmConfig>,
) -> Result<AgentName, AtmError> {
if let Some(actor) = actor_override.filter(|value| !value.trim().is_empty()) {
return resolve_aliased_agent(actor, config);
}
if let Some(identity) = hook::read_hook_identity()? {
return Ok(identity);
}
resolve_runtime_sender_identity(config)
}
pub(crate) fn resolve_sender_identity(
sender_override: Option<&str>,
config: Option<&AtmConfig>,
) -> Result<AgentName, AtmError> {
if let Some(sender) = sender_override.filter(|value| !value.trim().is_empty()) {
return resolve_aliased_agent(sender.trim(), config);
}
if let Some(identity) = hook::read_hook_identity()? {
return resolve_aliased_agent(identity.as_str(), config);
}
resolve_runtime_sender_identity(config)
}
pub fn resolve_runtime_sender_identity(config: Option<&AtmConfig>) -> Result<AgentName, AtmError> {
crate::config::resolve_identity(config).ok_or_else(AtmError::identity_unavailable)
}
fn resolve_aliased_agent(value: &str, config: Option<&AtmConfig>) -> Result<AgentName, AtmError> {
crate::config::aliases::resolve_agent(value, config).parse()
}
#[cfg(test)]
pub fn resolve_hook_identity(
team_override: Option<&str>,
config: Option<&AtmConfig>,
) -> Result<(String, String), AtmError> {
let agent = resolve_runtime_sender_identity(config)?;
let team = crate::config::resolve_team(team_override, config)
.ok_or_else(AtmError::team_unavailable)?;
Ok((agent.into_inner(), team.into_inner()))
}
#[cfg(test)]
mod tests {
use std::env;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::time::{SystemTime, UNIX_EPOCH};
use crate::config::AtmConfig;
use crate::types::AgentName;
use super::{resolve_hook_identity, resolve_runtime_sender_identity, resolve_sender_identity};
#[test]
#[serial_test::serial(env)]
fn resolves_sender_identity_from_environment() {
let original_identity = env::var_os("ATM_IDENTITY");
set_env_var("ATM_IDENTITY", "arch-ctm");
let config = AtmConfig {
identity: Some("config-agent".into()),
obsolete_identity_present: true,
..Default::default()
};
assert_eq!(
resolve_runtime_sender_identity(Some(&config)).expect("identity"),
AgentName::from_validated("arch-ctm")
);
restore("ATM_IDENTITY", original_identity);
}
#[test]
#[serial_test::serial(env)]
fn sender_identity_does_not_fall_back_to_config_when_env_missing() {
let original_identity = env::var_os("ATM_IDENTITY");
remove_env_var("ATM_IDENTITY");
let config = AtmConfig {
identity: Some("config-agent".into()),
obsolete_identity_present: true,
..Default::default()
};
let error = resolve_runtime_sender_identity(Some(&config)).expect_err("identity error");
assert!(error.is_identity());
restore("ATM_IDENTITY", original_identity);
}
#[test]
#[serial_test::serial(env)]
fn resolves_hook_identity_from_environment() {
let original_identity = env::var_os("ATM_IDENTITY");
let original_team = env::var_os("ATM_TEAM");
set_env_var("ATM_IDENTITY", "arch-ctm");
set_env_var("ATM_TEAM", "atm-dev");
let (agent, team) = resolve_hook_identity(None, None).expect("hook identity");
assert_eq!(agent, "arch-ctm");
assert_eq!(team, "atm-dev");
restore("ATM_IDENTITY", original_identity);
restore("ATM_TEAM", original_team);
}
#[test]
#[serial_test::serial(env)]
fn hook_identity_requires_runtime_identity_when_env_missing() {
let original_identity = env::var_os("ATM_IDENTITY");
let original_team = env::var_os("ATM_TEAM");
remove_env_var("ATM_IDENTITY");
set_env_var("ATM_TEAM", "");
let config = AtmConfig {
identity: Some("config-agent".into()),
default_team: Some("config-team".parse().expect("team")),
obsolete_identity_present: true,
..Default::default()
};
let error = resolve_hook_identity(None, Some(&config)).expect_err("hook identity error");
assert!(error.is_identity());
restore("ATM_IDENTITY", original_identity);
restore("ATM_TEAM", original_team);
}
#[cfg(unix)]
#[test]
#[serial_test::serial(env)]
fn send_sender_identity_applies_alias_to_hook_identity() {
let original_identity = env::var_os("ATM_IDENTITY");
remove_env_var("ATM_IDENTITY");
let hook_path =
std::env::temp_dir().join(format!("atm-hook-{}.json", unsafe { libc::getppid() }));
let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time")
.as_secs_f64();
fs::write(
&hook_path,
format!(r#"{{"agent_name":"lead","created_at":{created_at}}}"#),
)
.expect("hook file");
let mut aliases = std::collections::BTreeMap::new();
aliases.insert("lead".to_string(), "team-lead".to_string());
let config = AtmConfig {
aliases,
..Default::default()
};
assert_eq!(
resolve_sender_identity(None, Some(&config)).expect("send identity"),
AgentName::from_validated("team-lead")
);
let _ = fs::remove_file(hook_path);
restore("ATM_IDENTITY", original_identity);
}
fn restore(key: &str, value: Option<std::ffi::OsString>) {
match value {
Some(value) => set_env_var(key, value),
None => remove_env_var(key),
}
}
fn set_env_var<K: AsRef<std::ffi::OsStr>, V: AsRef<std::ffi::OsStr>>(key: K, value: V) {
unsafe { env::set_var(key, value) }
}
fn remove_env_var<K: AsRef<std::ffi::OsStr>>(key: K) {
unsafe { env::remove_var(key) }
}
}