use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::intent::{AgentId, IntentId, SessionId};
use crate::namespace::Namespace;
use crate::object::ObjectId;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SessionStatus {
Active,
Completed,
Abandoned,
}
impl SessionStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Active => "Active",
Self::Completed => "Completed",
Self::Abandoned => "Abandoned",
}
}
pub fn from_wire(s: &str) -> Self {
match s {
"Active" => Self::Active,
"Completed" => Self::Completed,
_ => Self::Abandoned,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: SessionId,
pub agent_id: AgentId,
pub working_branch: String,
pub head: ObjectId,
pub parent_session: Option<SessionId>,
pub delegated_intent: Option<IntentId>,
pub report_to: Option<String>,
pub path_scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope_tenant: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope_namespace: Option<Namespace>,
#[serde(default = "default_status")]
pub status: SessionStatus,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub ended_at: Option<DateTime<Utc>>,
}
fn default_status() -> SessionStatus {
SessionStatus::Active
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn as_str_active() {
assert_eq!(SessionStatus::Active.as_str(), "Active");
}
#[test]
fn as_str_completed() {
assert_eq!(SessionStatus::Completed.as_str(), "Completed");
}
#[test]
fn as_str_abandoned() {
assert_eq!(SessionStatus::Abandoned.as_str(), "Abandoned");
}
#[test]
fn from_wire_known_values() {
assert_eq!(SessionStatus::from_wire("Active"), SessionStatus::Active);
assert_eq!(
SessionStatus::from_wire("Completed"),
SessionStatus::Completed
);
assert_eq!(
SessionStatus::from_wire("Abandoned"),
SessionStatus::Abandoned
);
}
#[test]
fn from_wire_unknown_maps_to_abandoned() {
assert_eq!(
SessionStatus::from_wire("unknown-future-status"),
SessionStatus::Abandoned
);
assert_eq!(SessionStatus::from_wire(""), SessionStatus::Abandoned);
assert_eq!(
SessionStatus::from_wire("active"), SessionStatus::Abandoned
);
}
#[test]
fn as_str_from_wire_roundtrip() {
for s in [
SessionStatus::Active,
SessionStatus::Completed,
SessionStatus::Abandoned,
] {
assert_eq!(SessionStatus::from_wire(s.as_str()), s);
}
}
#[test]
fn session_status_serializes_and_deserializes() {
for s in [
SessionStatus::Active,
SessionStatus::Completed,
SessionStatus::Abandoned,
] {
let j = serde_json::to_string(&s).unwrap();
let back: SessionStatus = serde_json::from_str(&j).unwrap();
assert_eq!(back, s);
}
}
#[test]
fn session_optional_fields_skip_when_none() {
let s = Session {
id: "s1".into(),
agent_id: "agent/test".into(),
working_branch: "main".into(),
head: ObjectId::hash(b"head"),
parent_session: None,
delegated_intent: None,
report_to: None,
path_scope: None,
scope_tenant: None,
scope_namespace: None,
status: SessionStatus::Active,
created_at: Utc::now(),
ended_at: None,
};
let json = serde_json::to_string(&s).unwrap();
assert!(
!json.contains("scope_tenant"),
"scope_tenant should be omitted when None (has skip_serializing_if)"
);
assert!(
json.contains("ended_at"),
"ended_at is always present in JSON (serializes as null when None)"
);
}
#[test]
fn session_round_trip_with_all_fields() {
let s = Session {
id: "sess-42".into(),
agent_id: "agent/planner".into(),
working_branch: "feature/planning".into(),
head: ObjectId::hash(b"some-commit"),
parent_session: Some("sess-1".into()),
delegated_intent: Some("intent-7".into()),
report_to: Some("lead/coordinator".into()),
path_scope: Some("/cluster/nodes".into()),
scope_tenant: Some("tenant-A".into()),
scope_namespace: Some(Namespace::new("project-x").unwrap()),
status: SessionStatus::Completed,
created_at: Utc::now(),
ended_at: Some(Utc::now()),
};
let json = serde_json::to_string(&s).unwrap();
let restored: Session = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, s.id);
assert_eq!(restored.agent_id, s.agent_id);
assert_eq!(restored.working_branch, s.working_branch);
assert_eq!(restored.status, SessionStatus::Completed);
assert_eq!(restored.parent_session, s.parent_session);
assert_eq!(restored.scope_tenant.as_deref(), Some("tenant-A"));
assert_eq!(
restored.scope_namespace.as_ref().map(|n| n.as_str()),
Some("project-x")
);
assert!(restored.ended_at.is_some());
}
}