use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::event::BackendKind;
use super::id::ControlSessionId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionState {
Starting,
Running,
AwaitingAuth,
AwaitingIntervention,
Stopping,
Stopped,
Restarting { attempt: u8 },
Failed,
AuthFailed,
}
impl SessionState {
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Stopped | Self::Failed | Self::AuthFailed)
}
pub fn label(&self) -> &'static str {
match self {
Self::Starting => "starting",
Self::Running => "running",
Self::AwaitingAuth => "awaiting-auth",
Self::AwaitingIntervention => "awaiting-intervention",
Self::Stopping => "stopping",
Self::Stopped => "stopped",
Self::Restarting { .. } => "restarting",
Self::Failed => "failed",
Self::AuthFailed => "auth-failed",
}
}
}
impl std::fmt::Display for SessionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMetadata {
pub session_id: ControlSessionId,
pub project_id: String,
pub backend: BackendKind,
pub state: SessionState,
pub restart_count: u8,
pub context_lost: bool,
pub started_at: DateTime<Utc>,
pub last_activity_at: DateTime<Utc>,
pub pending_decision: Option<String>,
pub proposed_default: Option<String>,
pub last_summary: Option<String>,
}
impl SessionMetadata {
pub fn new(session_id: ControlSessionId, project_id: String, backend: BackendKind) -> Self {
let now = Utc::now();
Self {
session_id,
project_id,
backend,
state: SessionState::Starting,
restart_count: 0,
context_lost: false,
started_at: now,
last_activity_at: now,
pending_decision: None,
proposed_default: None,
last_summary: None,
}
}
pub fn uptime_secs(&self) -> u64 {
let delta = Utc::now() - self.started_at;
u64::try_from(delta.num_seconds()).unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_state_is_terminal() {
assert!(SessionState::Stopped.is_terminal());
assert!(SessionState::Failed.is_terminal());
assert!(SessionState::AuthFailed.is_terminal());
assert!(!SessionState::Running.is_terminal());
assert!(!SessionState::Starting.is_terminal());
assert!(!SessionState::Restarting { attempt: 2 }.is_terminal());
assert!(!SessionState::Stopping.is_terminal());
assert!(!SessionState::AwaitingAuth.is_terminal());
}
#[test]
fn session_state_display() {
assert_eq!(SessionState::Running.to_string(), "running");
assert_eq!(
SessionState::Restarting { attempt: 1 }.to_string(),
"restarting"
);
assert_eq!(
SessionState::AwaitingIntervention.to_string(),
"awaiting-intervention"
);
}
#[test]
fn session_metadata_serde() {
let id = ControlSessionId::new("p", 0);
let meta = SessionMetadata::new(id.clone(), "p".into(), BackendKind::StreamJson);
let json = serde_json::to_string(&meta).expect("serialize");
let back: SessionMetadata = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.session_id, id);
assert_eq!(back.state, SessionState::Starting);
assert_eq!(back.restart_count, 0);
assert!(!back.context_lost);
}
#[test]
fn session_metadata_uptime_non_negative() {
let meta =
SessionMetadata::new(ControlSessionId::new("x", 0), "x".into(), BackendKind::Tmux);
let _ = meta.uptime_secs();
}
}