use atm_core::{AgentType, HookEventType, SessionDomain, SessionId, SessionView};
use thiserror::Error;
use tokio::sync::oneshot;
#[derive(Debug)]
pub enum RegistryCommand {
Register {
session: Box<SessionDomain>,
respond_to: oneshot::Sender<Result<(), RegistryError>>,
},
UpdateFromStatusLine {
session_id: SessionId,
data: serde_json::Value,
respond_to: oneshot::Sender<Result<(), RegistryError>>,
},
ApplyHookEvent {
session_id: SessionId,
event_type: HookEventType,
tool_name: Option<String>,
notification_type: Option<String>,
pid: Option<u32>,
tmux_pane: Option<String>,
agent_id: Option<String>,
agent_type: Option<String>,
prompt: Option<String>,
respond_to: oneshot::Sender<Result<(), RegistryError>>,
},
GetSession {
session_id: SessionId,
respond_to: oneshot::Sender<Option<SessionView>>,
},
GetAllSessions {
respond_to: oneshot::Sender<Vec<SessionView>>,
},
Remove {
session_id: SessionId,
respond_to: oneshot::Sender<Result<(), RegistryError>>,
},
CleanupStale,
RefreshGitInfo,
RegisterDiscovered {
session_id: SessionId,
pid: u32,
cwd: std::path::PathBuf,
tmux_pane: Option<String>,
respond_to: oneshot::Sender<Result<(), RegistryError>>,
},
}
#[derive(Debug, Clone, Error)]
pub enum RegistryError {
#[error("registry is full (max: {max} sessions)")]
RegistryFull {
max: usize,
},
#[error("session not found: {0}")]
SessionNotFound(SessionId),
#[error("session already exists: {0}")]
SessionAlreadyExists(SessionId),
#[error("response channel closed")]
ChannelClosed,
#[error("parse error: {0}")]
ParseError(String),
}
impl RegistryError {
pub fn parse<E: std::fmt::Display>(err: E) -> Self {
Self::ParseError(err.to_string())
}
}
#[derive(Debug, Clone)]
pub enum SessionEvent {
Registered {
session_id: SessionId,
agent_type: AgentType,
},
Updated {
session: Box<SessionView>,
},
Removed {
session_id: SessionId,
reason: RemovalReason,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemovalReason {
Explicit,
RegistryFull,
SessionEnded,
ProcessDied,
Upgraded,
}
impl std::fmt::Display for RemovalReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Explicit => write!(f, "explicitly removed"),
Self::RegistryFull => write!(f, "registry capacity reached"),
Self::SessionEnded => write!(f, "session ended by Claude Code"),
Self::ProcessDied => write!(f, "process died without SessionEnd"),
Self::Upgraded => write!(f, "upgraded to real session"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use atm_core::Model;
#[test]
fn test_registry_error_display() {
let err = RegistryError::RegistryFull { max: 100 };
assert_eq!(err.to_string(), "registry is full (max: 100 sessions)");
let err = RegistryError::SessionNotFound(SessionId::new("test-123"));
assert_eq!(err.to_string(), "session not found: test-123");
let err = RegistryError::SessionAlreadyExists(SessionId::new("test-456"));
assert_eq!(err.to_string(), "session already exists: test-456");
let err = RegistryError::ChannelClosed;
assert_eq!(err.to_string(), "response channel closed");
let err = RegistryError::ParseError("invalid JSON".to_string());
assert_eq!(err.to_string(), "parse error: invalid JSON");
}
#[test]
fn test_registry_error_parse_helper() {
let err = RegistryError::parse("something went wrong");
assert!(matches!(err, RegistryError::ParseError(_)));
assert_eq!(err.to_string(), "parse error: something went wrong");
}
#[test]
fn test_removal_reason_display() {
assert_eq!(RemovalReason::Explicit.to_string(), "explicitly removed");
assert_eq!(
RemovalReason::RegistryFull.to_string(),
"registry capacity reached"
);
assert_eq!(
RemovalReason::SessionEnded.to_string(),
"session ended by Claude Code"
);
assert_eq!(
RemovalReason::ProcessDied.to_string(),
"process died without SessionEnd"
);
}
#[test]
fn test_session_event_variants() {
let registered = SessionEvent::Registered {
session_id: SessionId::new("test-1"),
agent_type: AgentType::GeneralPurpose,
};
let _cloned = registered.clone();
let session =
SessionDomain::new(SessionId::new("test-2"), AgentType::Explore, Model::Sonnet4);
let updated = SessionEvent::Updated {
session: Box::new(SessionView::from_domain(&session)),
};
let _cloned = updated.clone();
let removed = SessionEvent::Removed {
session_id: SessionId::new("test-3"),
reason: RemovalReason::ProcessDied,
};
let _cloned = removed.clone();
}
#[tokio::test]
async fn test_command_oneshot_pattern() {
let (tx, rx) = oneshot::channel::<Result<(), RegistryError>>();
tokio::spawn(async move {
tx.send(Ok(())).ok();
});
let result = rx.await;
assert!(result.is_ok());
assert!(result.unwrap().is_ok());
}
#[tokio::test]
async fn test_command_channel_closed_error() {
let (tx, rx) = oneshot::channel::<Result<(), RegistryError>>();
drop(tx);
let result = rx.await;
assert!(result.is_err());
}
}