use std::future::IntoFuture as _;
use super::{format_state_column, format_tombstone_row, short_timestamp, truncate};
use super::{session_activity, session_decommission, session_resume, session_stop};
#[test]
fn truncate_clips_and_appends_ellipsis() {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("hello world", 5), "hell\u{2026}");
assert_eq!(truncate("", 5), "");
assert_eq!(truncate("abcde", 5), "abcde");
}
#[test]
fn format_state_column_appends_dead_marker() {
assert_eq!(
format_state_column("stopped", true, false),
"stopped [dead]"
);
assert_eq!(
format_state_column("errored", true, false),
"errored [dead]"
);
}
#[test]
fn format_state_column_appends_stale_assets_marker() {
assert_eq!(
format_state_column("active", false, true),
"active [stale-assets]"
);
assert_eq!(
format_state_column("stopped", true, true),
"stopped [dead] [stale-assets]",
"both markers must be able to appear together"
);
}
#[test]
fn format_state_column_leaves_healthy_state_unchanged() {
assert_eq!(format_state_column("active", false, false), "active");
assert_eq!(format_state_column("stopped", false, false), "stopped");
assert_eq!(
format_state_column("provisioning", false, false),
"provisioning"
);
}
#[test]
fn format_tombstone_row_shows_slot_and_placeholder() {
assert_eq!(format_tombstone_row(1), "1 -- deleted --");
assert_eq!(format_tombstone_row(42), "42 -- deleted --");
}
#[test]
fn format_state_column_renders_deleted_marker() {
assert_eq!(format_state_column("deleted", false, false), "--deleted--");
assert_eq!(
format_state_column("deleted", true, false),
"--deleted-- [dead]"
);
}
#[test]
fn short_timestamp_formats_correctly() {
assert_eq!(short_timestamp("2025-06-27T14:32:00Z"), "2025-06-27 14:32");
assert_eq!(short_timestamp("short"), "short");
assert_eq!(short_timestamp("2025-06-27T14:32"), "2025-06-27 14:32");
}
#[test]
fn decommission_message_reflects_workspace_removed() {
let owned_removed = serde_json::json!({
"id": "abc-123",
"workspace_removed": true,
"workspace_path_was": "/some/workspace/path"
});
assert_eq!(
owned_removed
.get("workspace_removed")
.and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
owned_removed
.get("workspace_path_was")
.and_then(|v| v.as_str()),
Some("/some/workspace/path")
);
let adopted_not_removed = serde_json::json!({
"id": "xyz-456",
"workspace_removed": false
});
assert_eq!(
adopted_not_removed
.get("workspace_removed")
.and_then(|v| v.as_bool()),
Some(false)
);
assert!(adopted_not_removed.get("workspace_path_was").is_none());
}
async fn spawn_test_daemon() -> String {
use trusty_mpm::daemon::{api, state::DaemonState};
let root = tempfile::tempdir().unwrap().keep();
let state = std::sync::Arc::new(DaemonState::with_root_isolated_managed(root).await);
let router = api::router(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, router).into_future());
format!("http://{addr}")
}
#[tokio::test]
async fn session_stop_not_found_errors() {
let url = spawn_test_daemon().await;
let client = reqwest::Client::new();
let err = session_stop(&client, &url, "nonexistent-id".to_string())
.await
.expect_err("a missing managed session must be a hard failure, not a silent Ok(())");
assert!(
err.to_string().contains("nonexistent-id"),
"error should name the missing id: {err}"
);
}
#[tokio::test]
async fn session_resume_not_found_errors() {
let url = spawn_test_daemon().await;
let client = reqwest::Client::new();
let err = session_resume(&client, &url, "nonexistent-id".to_string())
.await
.expect_err("a missing managed session must be a hard failure, not a silent Ok(())");
assert!(
err.to_string().contains("nonexistent-id"),
"error should name the missing id: {err}"
);
}
async fn spawn_test_daemon_with_unrestartable_stopped_session() -> (String, String) {
use trusty_mpm::daemon::{api, state::DaemonState};
use trusty_mpm::runtime::RuntimeKind;
use trusty_mpm::session_manager::ManagedSessionId;
let root = tempfile::tempdir().unwrap().keep();
let state = std::sync::Arc::new(DaemonState::with_root_isolated_managed(root.clone()).await);
let id = ManagedSessionId::new();
let ws = root.join(format!("{id}-resume-missing-ws"));
let mgr = state.session_manager().await;
mgr.create_with_id(
id,
"regression: #2649 resume-restart-failure CLI test".to_string(),
Some(ws.clone()),
None,
Some(ws.clone()),
Some("https://example.com/r.git".to_string()),
Some("main".to_string()),
RuntimeKind::default(),
false,
false,
)
.await
.expect("seed session");
mgr.stop(&id).await.expect("mark seeded session Stopped");
assert!(
!ws.exists(),
"#3715: stop()'s snapshot capture must NOT recreate a vanished workspace root"
);
let router = api::router(std::sync::Arc::clone(&state));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, router).into_future());
(format!("http://{addr}"), id.to_string())
}
#[tokio::test]
async fn session_resume_restart_failure_errors() {
let (url, id) = spawn_test_daemon_with_unrestartable_stopped_session().await;
let client = reqwest::Client::new();
let err = session_resume(&client, &url, id)
.await
.expect_err("a daemon-rejected restart must be a hard failure, not Ok(())");
assert!(
err.to_string().contains("cannot restart"),
"error should surface the daemon's rejection: {err}"
);
}
#[tokio::test]
async fn session_resume_headless_active_live_tmux_skips_restart_and_attach() {
use trusty_mpm::daemon::{api, state::DaemonState};
use trusty_mpm::runtime::RuntimeKind;
use trusty_mpm::session_manager::ManagedSessionId;
let root = tempfile::tempdir().unwrap().keep();
let state = std::sync::Arc::new(DaemonState::with_root_isolated_managed(root.clone()).await);
let id = ManagedSessionId::new();
let ws = root.join(format!("{id}-headless-attach-ws"));
let mgr = state.session_manager().await;
let record = mgr
.create_with_id(
id,
"regression: #2649 headless active-live-tmux CLI test".to_string(),
Some(ws.clone()),
None,
Some(ws),
Some("https://example.com/r.git".to_string()),
Some("main".to_string()),
RuntimeKind::default(),
false,
false,
)
.await
.expect("seed session");
assert_eq!(
record.state.to_string(),
"provisioning",
"sanity: create_with_id must leave a fresh record un-stopped/un-errored"
);
let tmux_bin = trusty_mpm::core::tmux::resolve_tmux_binary_or_bare();
let create_status = std::process::Command::new(&tmux_bin)
.args(["new-session", "-d", "-s", &record.tmux_name])
.status()
.expect("spawn real tmux session for the liveness probe");
assert!(
create_status.success(),
"failed to create the real scratch tmux session"
);
let router = api::router(std::sync::Arc::clone(&state));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, router).into_future());
let url = format!("http://{addr}");
let client = reqwest::Client::new();
let result = session_resume(&client, &url, id.to_string()).await;
let _ = std::process::Command::new(&tmux_bin)
.args(["kill-session", "-t", &record.tmux_name])
.output();
result.expect(
"headless resume of an already-live, non-stopped/errored session must succeed (Ok), \
not error",
);
let after: serde_json::Value = client
.get(format!("{url}/api/v1/sessions/managed/{id}"))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(
after.get("state").and_then(|v| v.as_str()),
Some("provisioning"),
"state must be UNCHANGED — a /resume POST would have flipped it to 'active'; this \
proves the Attach branch never issued a daemon-side restart"
);
}
#[tokio::test]
async fn session_resume_zombie_active_tmux_absent_reconciles_and_restarts() {
use trusty_mpm::daemon::{api, state::DaemonState};
use trusty_mpm::runtime::RuntimeKind;
use trusty_mpm::session_manager::{ManagedSessionId, ManagedSessionState};
let root = tempfile::tempdir().unwrap().keep();
let state = std::sync::Arc::new(DaemonState::with_root_isolated_managed(root.clone()).await);
let id = ManagedSessionId::new();
let ws = root.join(format!("{id}-zombie-ws"));
tokio::fs::create_dir_all(&ws)
.await
.expect("create a REAL workspace dir so the reconcile/restart attempt is not blocked");
let mgr = state.session_manager().await;
mgr.create_with_id(
id,
"regression: #3531 zombie-restart CLI test".to_string(),
Some(ws.clone()),
None,
Some(ws.clone()),
Some("https://example.com/r.git".to_string()),
Some("main".to_string()),
RuntimeKind::default(),
false,
false,
)
.await
.expect("seed session");
mgr.set_workspace(&id, ws, ManagedSessionState::Active)
.await
.expect("force record to Active (simulating a zombie)");
let router = api::router(std::sync::Arc::clone(&state));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, router).into_future());
let url = format!("http://{addr}");
let client = reqwest::Client::new();
let before: serde_json::Value = client
.get(format!("{url}/api/v1/sessions/managed/{id}"))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(
before.get("state").and_then(|v| v.as_str()),
Some("stopped"),
"sanity: the list/get endpoint's display reconciliation must show \
'stopped' for an Active record with no live tmux (the #3302 behavior)"
);
assert_eq!(
before.get("persisted_state").and_then(|v| v.as_str()),
Some("active"),
"sanity: persisted_state must still carry the TRUE record state"
);
if let Err(e) = session_resume(&client, &url, id.to_string()).await {
let msg = e.to_string();
assert!(
!msg.contains("cannot resume a session in state"),
"session_resume must never dead-end on the daemon's raw 409 for a \
zombie session — the #3531 regression: {msg}"
);
}
let after: serde_json::Value = client
.get(format!("{url}/api/v1/sessions/managed/{id}"))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_ne!(
after.get("persisted_state").and_then(|v| v.as_str()),
None,
"persisted_state must still be present on the wire"
);
let final_state = after.get("persisted_state").and_then(|v| v.as_str());
assert!(
matches!(final_state, Some("active") | Some("errored")),
"a genuine reconcile+restart attempt must land the record in 'active' \
(spawn succeeded) or 'errored' (spawn failed for environment reasons) \
— never left untouched; got persisted_state={final_state:?}"
);
}
#[tokio::test]
async fn session_decommission_not_found_errors() {
let url = spawn_test_daemon().await;
let client = reqwest::Client::new();
let err = session_decommission(&client, &url, "nonexistent-id".to_string())
.await
.expect_err("a missing managed session must be a hard failure, not a silent Ok(())");
assert!(
err.to_string().contains("nonexistent-id"),
"error should name the missing id: {err}"
);
}
#[tokio::test]
async fn session_activity_not_found_errors() {
let url = spawn_test_daemon().await;
let client = reqwest::Client::new();
let err = session_activity(&client, &url, "nonexistent-id".to_string())
.await
.expect_err("a missing managed session must be a hard failure, not a silent Ok(())");
assert!(
err.to_string().contains("nonexistent-id"),
"error should name the missing id: {err}"
);
}