use super::*;
#[test]
fn managed_session_id_round_trip() {
let id = ManagedSessionId::new();
let json = serde_json::to_string(&id).expect("serialize");
let back: ManagedSessionId = serde_json::from_str(&json).expect("deserialize");
assert_eq!(id, back);
assert_eq!(id.as_uuid(), back.as_uuid());
}
#[test]
fn state_display() {
assert_eq!(
ManagedSessionState::Provisioning.to_string(),
"provisioning"
);
assert_eq!(ManagedSessionState::Active.to_string(), "active");
assert_eq!(ManagedSessionState::Stopped.to_string(), "stopped");
assert_eq!(ManagedSessionState::Errored.to_string(), "errored");
assert_eq!(
ManagedSessionState::Decommissioned.to_string(),
"decommissioned"
);
assert_eq!(ManagedSessionState::Deleted.to_string(), "deleted");
}
#[test]
fn record_serde_round_trip() {
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-quiet-falcon".into(),
cwd: PathBuf::from("/tmp/project"),
task: "implement feature X".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: Some(Utc::now()),
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.id, record.id);
assert_eq!(back.tmux_name, record.tmux_name);
assert_eq!(back.state, record.state);
}
#[test]
fn stopped_state_survives_serde() {
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-test".into(),
cwd: PathBuf::from("/tmp"),
task: "task".into(),
state: ManagedSessionState::Stopped,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(PathBuf::from("/tmp/ws")),
repo_url: Some("https://github.com/owner/repo".into()),
branch: Some("main".into()),
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.state, ManagedSessionState::Stopped);
assert_eq!(back.workspace_path, record.workspace_path);
}
#[test]
fn decommissioned_state_survives_serde() {
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-gone".into(),
cwd: PathBuf::from("/tmp"),
task: "task".into(),
state: ManagedSessionState::Decommissioned,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None, repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.state, ManagedSessionState::Decommissioned);
assert!(back.workspace_path.is_none());
}
#[test]
fn record_without_runtime_field_defaults_to_claude_code() {
let legacy_json = serde_json::json!({
"id": ManagedSessionId::new(),
"tmux_name": "tmpm-legacy",
"cwd": "/tmp",
"task": "legacy task",
"state": "active",
"created_at": Utc::now().to_rfc3339(),
"last_activity_at": null,
"workspace_path": null,
"repo_url": null,
"branch": null,
"pending_decision": null,
"proposed_default": null
})
.to_string();
let back: SessionRecord = serde_json::from_str(&legacy_json).expect("deserialize legacy");
assert_eq!(back.runtime, crate::runtime::RuntimeKind::ClaudeCode);
}
#[test]
fn record_round_trips_tcode_runtime() {
let mut record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-tcode".into(),
cwd: PathBuf::from("/tmp"),
task: "task".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
record.runtime = crate::runtime::RuntimeKind::Tcode;
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.runtime, crate::runtime::RuntimeKind::Tcode);
}
#[test]
fn record_without_ephemeral_field_defaults_to_false() {
let legacy_json = serde_json::json!({
"id": ManagedSessionId::new(),
"tmux_name": "tmpm-legacy",
"cwd": "/tmp",
"task": "legacy task",
"state": "stopped",
"created_at": Utc::now().to_rfc3339(),
"last_activity_at": null,
"workspace_path": null,
"repo_url": null,
"branch": null,
"pending_decision": null,
"proposed_default": null
})
.to_string();
let back: SessionRecord = serde_json::from_str(&legacy_json).expect("deserialize legacy");
assert!(
!back.ephemeral,
"a record with no `ephemeral` key must default to false (non-ephemeral)"
);
}
#[test]
fn record_round_trips_ephemeral_true() {
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-ephemeral".into(),
cwd: PathBuf::from("/tmp"),
task: "throwaway".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: true,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert!(back.ephemeral, "ephemeral=true must round-trip");
}
#[test]
fn record_without_workspace_owned_field_defaults_to_false() {
let legacy_json = serde_json::json!({
"id": ManagedSessionId::new(),
"tmux_name": "tmpm-legacy",
"cwd": "/tmp",
"task": "legacy task",
"state": "stopped",
"created_at": Utc::now().to_rfc3339(),
"last_activity_at": null,
"workspace_path": "/tmp/some-workspace",
"repo_url": null,
"branch": null,
"pending_decision": null,
"proposed_default": null
})
.to_string();
let back: SessionRecord = serde_json::from_str(&legacy_json).expect("deserialize legacy");
assert!(
!back.workspace_owned,
"a record with no `workspace_owned` key must default to false (unowned — safe)"
);
}
#[test]
fn record_without_scrollback_fields_defaults_to_none() {
let legacy_json = serde_json::json!({
"id": ManagedSessionId::new(),
"tmux_name": "tmpm-legacy",
"cwd": "/tmp",
"task": "legacy task",
"state": "stopped",
"created_at": Utc::now().to_rfc3339(),
"last_activity_at": null,
"workspace_path": "/tmp/ws",
"repo_url": null,
"branch": null,
"pending_decision": null,
"proposed_default": null
})
.to_string();
let back: SessionRecord = serde_json::from_str(&legacy_json).expect("deserialize legacy");
assert!(
back.scrollback_path.is_none(),
"scrollback_path must default to None for legacy records"
);
assert!(
back.last_cwd.is_none(),
"last_cwd must default to None for legacy records"
);
}
#[test]
fn record_round_trips_scrollback_fields() {
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-snap".into(),
cwd: PathBuf::from("/home/user/project"),
task: "add feature".into(),
state: ManagedSessionState::Stopped,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(PathBuf::from("/managed/ws")),
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: true,
source_id: None,
claude_session_id: None,
scrollback_path: Some(PathBuf::from("/managed/ws/.trusty-mpm/scrollback.txt")),
last_cwd: Some(PathBuf::from("/managed/ws/src")),
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(
back.scrollback_path,
Some(PathBuf::from("/managed/ws/.trusty-mpm/scrollback.txt"))
);
assert_eq!(back.last_cwd, Some(PathBuf::from("/managed/ws/src")));
}
#[test]
fn record_round_trips_workspace_owned_true() {
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-clone".into(),
cwd: PathBuf::from("/managed/root/owner/repo/abc"),
task: "fix bug".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(PathBuf::from("/managed/root/owner/repo/abc")),
repo_url: Some("https://github.com/owner/repo".into()),
branch: Some("fix/thing".into()),
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: true,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert!(back.workspace_owned, "workspace_owned=true must round-trip");
}
#[test]
fn record_without_deliverable_id_field_defaults_to_none() {
let legacy_json = serde_json::json!({
"id": ManagedSessionId::new(),
"tmux_name": "tmpm-legacy",
"cwd": "/tmp",
"task": "legacy task",
"state": "active",
"created_at": Utc::now().to_rfc3339(),
"last_activity_at": null,
"workspace_path": null,
"repo_url": null,
"branch": null,
"pending_decision": null,
"proposed_default": null
})
.to_string();
let back: SessionRecord = serde_json::from_str(&legacy_json).expect("deserialize legacy");
assert!(
back.deliverable_id.is_none(),
"a record with no `deliverable_id` key must default to None (unbound)"
);
}
#[test]
fn record_round_trips_deliverable_id() {
let did = crate::deliverable::DeliverableId::new();
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-bound".into(),
cwd: PathBuf::from("/tmp"),
task: "implement WI-13".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: Some(did),
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.deliverable_id, Some(did));
}
#[test]
fn record_without_worktree_owner_field_defaults_to_none() {
let legacy_json = serde_json::json!({
"id": ManagedSessionId::new(),
"tmux_name": "tmpm-legacy",
"cwd": "/tmp",
"task": "legacy task",
"state": "active",
"created_at": Utc::now().to_rfc3339(),
"last_activity_at": null,
"workspace_path": null,
"repo_url": null,
"branch": null,
"pending_decision": null,
"proposed_default": null
})
.to_string();
let back: SessionRecord = serde_json::from_str(&legacy_json).expect("deserialize legacy");
assert!(
back.worktree_owner.is_none(),
"a record with no `worktree_owner` key must default to None (owner-unknown)"
);
}
#[test]
fn record_round_trips_worktree_owner() {
let owner = ManagedSessionId::new();
let record = SessionRecord {
id: owner,
tmux_name: "tmpm-owned".into(),
cwd: PathBuf::from("/tmp"),
task: "task".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: Some(owner),
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: SessionRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.worktree_owner, Some(owner));
}
#[test]
fn is_terminal_covers_tombstones() {
assert!(ManagedSessionState::Decommissioned.is_terminal());
assert!(ManagedSessionState::Deleted.is_terminal());
assert!(!ManagedSessionState::Provisioning.is_terminal());
assert!(!ManagedSessionState::Active.is_terminal());
assert!(!ManagedSessionState::Stopped.is_terminal());
assert!(!ManagedSessionState::Errored.is_terminal());
}
#[test]
fn from_wire_round_trips_every_variant() {
for state in [
ManagedSessionState::Provisioning,
ManagedSessionState::Active,
ManagedSessionState::Stopped,
ManagedSessionState::Errored,
ManagedSessionState::Decommissioned,
ManagedSessionState::Deleted,
] {
assert_eq!(
ManagedSessionState::from_wire(&state.to_string()),
Some(state.clone()),
"from_wire must round-trip {state:?}"
);
}
assert_eq!(ManagedSessionState::from_wire("bogus"), None);
assert_eq!(ManagedSessionState::from_wire(""), None);
}
#[test]
fn leaked_test_adoption_requires_both_a_reserved_name_and_an_adopted_task() {
let reserved = format!(
"{}deadrt1234-01",
trusty_common::session_naming::RESERVED_TEST_PREFIX
);
let mut adopted = terminal_fixture();
adopted.tmux_name = reserved.clone();
adopted.task = ADOPTED_TASK.into();
assert!(adopted.is_leaked_test_adoption());
let mut errored = adopted.clone();
errored.task = format!("{ADOPTED_TASK} [error: boom]");
assert!(errored.is_leaked_test_adoption());
let mut created = terminal_fixture();
created.tmux_name = reserved;
created.task = "implement feature X".into();
assert!(
!created.is_leaked_test_adoption(),
"a created session in the namespace is real work, not a leak"
);
let mut ordinary = terminal_fixture();
ordinary.tmux_name = "tm-trusty-tools-01".into();
ordinary.task = ADOPTED_TASK.into();
assert!(!ordinary.is_leaked_test_adoption());
}
#[test]
fn a_stopped_leaked_test_adoption_is_never_auto_resumable() {
let reserved = format!(
"{}deadrt1234-01",
trusty_common::session_naming::RESERVED_TEST_PREFIX
);
let mut leaked = terminal_fixture();
leaked.tmux_name = reserved.clone();
leaked.task = ADOPTED_TASK.into();
leaked.state = ManagedSessionState::Stopped;
leaked.stop_cause = Some(StopCause::Unexpected);
assert!(
!leaked.is_auto_resumable(),
"resuming this recreates the tmux session the next reconcile re-adopts"
);
let mut created = leaked.clone();
created.task = "implement feature X".into();
assert!(
created.is_auto_resumable(),
"an ordinary session in the namespace keeps post-reboot restore"
);
}
fn terminal_fixture() -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tm-terminal-fixture".into(),
cwd: PathBuf::from("/tmp"),
task: "t".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
}
}
#[test]
fn set_lifecycle_state_stamps_once() {
let mut r = terminal_fixture();
let first = Utc::now();
r.set_lifecycle_state(ManagedSessionState::Decommissioned, first);
assert_eq!(r.state, ManagedSessionState::Decommissioned);
assert_eq!(r.terminal_at, Some(first));
let later = first + chrono::Duration::days(3);
r.set_lifecycle_state(ManagedSessionState::Decommissioned, later);
assert_eq!(
r.terminal_at,
Some(first),
"re-entering a terminal state must not extend retention"
);
}
#[test]
fn set_lifecycle_state_clears_stamp_on_revival() {
let mut r = terminal_fixture();
let died = Utc::now();
r.set_lifecycle_state(ManagedSessionState::Decommissioned, died);
r.set_lifecycle_state(
ManagedSessionState::Stopped,
died + chrono::Duration::hours(1),
);
assert_eq!(r.terminal_at, None, "a revived record has no death time");
let again = died + chrono::Duration::days(2);
r.set_lifecycle_state(ManagedSessionState::Deleted, again);
assert_eq!(r.terminal_at, Some(again), "a fresh death restamps");
}