use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::types::{AgentId, AgentName, ModelName, PaneId};
pub use atm_storage::contract::AgentType;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentMember {
pub name: AgentName,
#[serde(default, skip_serializing_if = "AgentId::is_empty")]
pub agent_id: AgentId,
#[serde(default)]
pub agent_type: AgentType,
#[serde(default)]
pub model: ModelName,
#[serde(default)]
pub joined_at: Option<u64>,
#[serde(default)]
pub tmux_pane_id: Option<PaneId>,
#[serde(default)]
pub cwd: PathBuf,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl AgentMember {
pub fn with_name(name: AgentName) -> Self {
Self {
name,
agent_id: AgentId::default(),
agent_type: AgentType::default(),
model: ModelName::default(),
joined_at: None,
tmux_pane_id: None,
cwd: PathBuf::new(),
extra: Map::new(),
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{AgentMember, AgentType};
use crate::test_support::{TEST_SENDER, TEST_TEAM};
use crate::types::AgentName;
#[test]
fn parse_name_only_record_defaults_optional_fields() {
let member: AgentMember =
serde_json::from_str(&format!(r#"{{"name":"{TEST_SENDER}"}}"#)).expect("member");
assert_eq!(member.name, AgentName::from_validated(TEST_SENDER));
assert!(member.agent_id.is_empty());
assert_eq!(member.agent_type, AgentType::Unknown(String::new()));
assert!(member.model.is_empty());
assert_eq!(member.joined_at, None);
assert_eq!(member.tmux_pane_id, None);
assert_eq!(member.cwd, PathBuf::new());
assert!(member.extra.is_empty());
}
#[test]
fn parse_full_claude_code_record_preserves_values_and_extra() {
let raw = format!(
r#"{{
"agentId":"{TEST_SENDER}@{TEST_TEAM}",
"name":"{TEST_SENDER}",
"agentType":"general-purpose",
"model":"claude-sonnet-4-5",
"joinedAt":1770765919076,
"tmuxPaneId":"%1",
"cwd":"/workspace",
"color":"blue"
}}"#
);
let member: AgentMember = serde_json::from_str(&raw).expect("member");
assert_eq!(
member.agent_id.as_str(),
format!("{TEST_SENDER}@{TEST_TEAM}")
);
assert_eq!(member.name, AgentName::from_validated(TEST_SENDER));
assert_eq!(member.agent_type, AgentType::GeneralPurpose);
assert_eq!(member.model.as_str(), "claude-sonnet-4-5");
assert_eq!(member.joined_at, Some(1770765919076));
assert_eq!(member.tmux_pane_id.as_deref(), Some("%1"));
assert_eq!(member.cwd, PathBuf::from("/workspace"));
assert_eq!(member.extra["color"], serde_json::json!("blue"));
let encoded = serde_json::to_string(&member).expect("encode");
let decoded: AgentMember = serde_json::from_str(&encoded).expect("decode");
assert_eq!(decoded, member);
}
#[test]
fn parse_name_and_agent_type_record_succeeds() {
let member: AgentMember =
serde_json::from_str(&format!(r#"{{"name":"{TEST_SENDER}","agentType":"plan"}}"#))
.expect("member");
assert_eq!(member.name, AgentName::from_validated(TEST_SENDER));
assert_eq!(member.agent_type, AgentType::Plan);
assert!(member.agent_id.is_empty());
assert!(member.model.is_empty());
assert_eq!(member.joined_at, None);
assert_eq!(member.tmux_pane_id, None);
assert_eq!(member.cwd, PathBuf::new());
}
}