use std::future::IntoFuture as _;
use super::{format_state_column, 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_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");
tokio::fs::remove_dir_all(&ws)
.await
.expect("remove the dir stop()'s snapshot capture recreated, so it is genuinely absent");
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_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}"
);
}