use trusty_mpm::client::{
CommandExecutor, CommandResult, ManagedSessionSummary, TrustyCommand, resolve_target,
};
use crate::cli::SessionAction;
pub(crate) fn executor(daemon: &trusty_mpm::client::DaemonClient) -> CommandExecutor {
CommandExecutor::from_daemon_client(daemon.clone())
}
pub(crate) fn to_command(action: &SessionAction) -> Option<TrustyCommand> {
Some(match action {
SessionAction::New {
repo,
git_ref,
task,
name_hint,
runtime,
no_inject,
deliverable,
} => TrustyCommand::ManagedNew {
repo_url: repo.clone(),
git_ref: git_ref.clone(),
task: task.clone(),
name_hint: name_hint.clone(),
runtime: Some(runtime.as_str().to_string()),
inject_task: if *no_inject { Some(false) } else { None },
deliverable_id: deliverable.clone(),
force_new: true,
},
SessionAction::Ls { .. } => TrustyCommand::ManagedList,
SessionAction::Send { id, text } => TrustyCommand::ManagedSend {
target: id.clone(),
text: text.clone(),
},
SessionAction::Answer { id, answer } => TrustyCommand::ManagedAnswer {
target: id.clone(),
answer: answer.clone(),
},
SessionAction::Attach { id } => TrustyCommand::ManagedAttachCmd { target: id.clone() },
SessionAction::ManagedStop { id } | SessionAction::RuntimeStop { id } => {
TrustyCommand::ManagedRuntimeStop { target: id.clone() }
}
SessionAction::ManagedResume { id } => TrustyCommand::ManagedResume { target: id.clone() },
SessionAction::Decommission { id, .. } => {
TrustyCommand::ManagedDecommission { target: id.clone() }
}
_ => return None,
})
}
pub(crate) fn decommission_message(id: &str, workspace_removed: Option<bool>) -> String {
match workspace_removed {
Some(true) => format!("decommissioned {id} — workspace removed; tombstone record kept"),
Some(false) => format!(
"decommissioned {id} — tombstone record kept; workspace NOT removed \
(still on disk)"
),
None => format!(
"decommissioned {id} — tombstone record kept; workspace outcome not \
reported by the daemon"
),
}
}
pub(crate) fn render_cli(result: &CommandResult) -> String {
match result {
CommandResult::ManagedSpawned {
id,
name,
state,
runtime,
attach_cmd,
} => format!("spawned {name} ({id}) [{state}] runtime={runtime}\n attach: {attach_cmd}"),
CommandResult::ManagedSessions(list) => {
if list.is_empty() {
return "no managed sessions".to_string();
}
list.iter()
.map(|s| {
let pending = s
.pending_decision
.as_deref()
.map(|d| format!(" pending=\"{d}\""))
.unwrap_or_default();
format!("{} {} {}{}", s.id, s.name, s.state, pending)
})
.collect::<Vec<_>>()
.join("\n")
}
CommandResult::ManagedSent { .. } => "sent".to_string(),
CommandResult::ManagedAnswered { .. } => "answered".to_string(),
CommandResult::ManagedAttachCmd { attach_cmd, .. } => attach_cmd.clone(),
CommandResult::ManagedLifecycle {
id,
name,
state,
action,
workspace_removed,
} => match action.as_str() {
"stopped" => {
format!("runtime stopped {id} (workspace intact; use 'resume' to restart)")
}
"resumed" => format!("resumed {name} ({id}) [{state}]"),
"decommissioned" => decommission_message(id, *workspace_removed),
other => format!("{other} {id} ({name}) [{state}]"),
},
CommandResult::Error(msg) => msg.clone(),
other => format!("unexpected result: {other:?}"),
}
}
pub(crate) async fn resolve_managed_summary(
daemon: &trusty_mpm::client::DaemonClient,
id_or_name: &str,
) -> Option<ManagedSessionSummary> {
let sessions = executor(daemon)
.client()
.list_managed_sessions()
.await
.ok()?;
resolve_target(&sessions, id_or_name).cloned()
}
pub(crate) async fn resolve_managed_match(
daemon: &trusty_mpm::client::DaemonClient,
id_or_name: &str,
) -> Option<String> {
resolve_managed_summary(daemon, id_or_name)
.await
.map(|s| s.id)
}
pub(crate) async fn resolve_project_session_id(
daemon: &trusty_mpm::client::DaemonClient,
id_or_name: &str,
) -> anyhow::Result<Option<String>> {
let rows = executor(daemon).client().sessions().await?;
Ok(resolve_target(&rows, id_or_name).map(|r| r.id.0.to_string()))
}
pub(crate) async fn run(
daemon: &trusty_mpm::client::DaemonClient,
action: &SessionAction,
) -> anyhow::Result<bool> {
let Some(cmd) = to_command(action) else {
return Ok(false);
};
let result = executor(daemon).execute(cmd).await;
if let CommandResult::Error(msg) = &result {
anyhow::bail!("{msg}");
}
println!("{}", render_cli(&result));
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use trusty_mpm::client::ManagedSessionView;
#[test]
fn to_command_maps_new() {
let action = SessionAction::New {
repo: "https://example/r.git".to_string(),
git_ref: "main".to_string(),
task: "do it".to_string(),
name_hint: Some("api".to_string()),
runtime: trusty_mpm::runtime::RuntimeKind::ClaudeCode,
no_inject: false,
deliverable: None,
};
match to_command(&action) {
Some(TrustyCommand::ManagedNew {
repo_url,
git_ref,
task,
name_hint,
runtime,
inject_task,
deliverable_id,
force_new,
}) => {
assert_eq!(repo_url, "https://example/r.git");
assert_eq!(git_ref, "main");
assert_eq!(task, "do it");
assert_eq!(name_hint.as_deref(), Some("api"));
assert_eq!(runtime.as_deref(), Some("claude-code"));
assert_eq!(inject_task, None);
assert_eq!(deliverable_id, None);
assert!(force_new, "session new must force a fresh session");
}
other => panic!("expected ManagedNew, got {other:?}"),
}
}
#[test]
fn to_command_new_no_inject_sets_metadata_only() {
let action = SessionAction::New {
repo: "https://example/r.git".to_string(),
git_ref: "main".to_string(),
task: "do it".to_string(),
name_hint: None,
runtime: trusty_mpm::runtime::RuntimeKind::ClaudeCode,
no_inject: true,
deliverable: None,
};
match to_command(&action) {
Some(TrustyCommand::ManagedNew { inject_task, .. }) => {
assert_eq!(inject_task, Some(false));
}
other => panic!("expected ManagedNew, got {other:?}"),
}
}
#[test]
fn to_command_new_deliverable_maps_to_deliverable_id() {
let action = SessionAction::New {
repo: "https://example/r.git".to_string(),
git_ref: "main".to_string(),
task: "do it".to_string(),
name_hint: None,
runtime: trusty_mpm::runtime::RuntimeKind::ClaudeCode,
no_inject: false,
deliverable: Some("11111111-1111-1111-1111-111111111111".to_string()),
};
match to_command(&action) {
Some(TrustyCommand::ManagedNew { deliverable_id, .. }) => {
assert_eq!(
deliverable_id,
Some("11111111-1111-1111-1111-111111111111".to_string())
);
}
other => panic!("expected ManagedNew, got {other:?}"),
}
}
#[test]
fn to_command_maps_send_answer_attach() {
assert!(matches!(
to_command(&SessionAction::Send {
id: "x".into(),
text: "hi".into()
}),
Some(TrustyCommand::ManagedSend { .. })
));
assert!(matches!(
to_command(&SessionAction::Answer {
id: "x".into(),
answer: "yes".into()
}),
Some(TrustyCommand::ManagedAnswer { .. })
));
assert!(matches!(
to_command(&SessionAction::Attach { id: "x".into() }),
Some(TrustyCommand::ManagedAttachCmd { .. })
));
}
#[test]
fn to_command_maps_lifecycle_aliases() {
assert!(matches!(
to_command(&SessionAction::ManagedStop { id: "x".into() }),
Some(TrustyCommand::ManagedRuntimeStop { .. })
));
assert!(matches!(
to_command(&SessionAction::RuntimeStop { id: "x".into() }),
Some(TrustyCommand::ManagedRuntimeStop { .. })
));
assert!(matches!(
to_command(&SessionAction::ManagedResume { id: "x".into() }),
Some(TrustyCommand::ManagedResume { .. })
));
assert!(matches!(
to_command(&SessionAction::Decommission {
id: "x".into(),
force: false,
}),
Some(TrustyCommand::ManagedDecommission { .. })
));
assert!(matches!(
to_command(&SessionAction::Ls {
terms: vec![],
json: false,
source_id: None,
current: false,
all: false,
no_prune: false,
}),
Some(TrustyCommand::ManagedList)
));
}
#[test]
fn to_command_ignores_project_verbs() {
assert!(
to_command(&SessionAction::Stop {
id_or_name: "x".into()
})
.is_none()
);
assert!(
to_command(&SessionAction::Resume {
id_or_name: "x".into()
})
.is_none()
);
assert!(to_command(&SessionAction::Breakers).is_none());
assert!(
to_command(&SessionAction::PruneIdle {
dry_run: false,
json: false
})
.is_none()
);
}
fn view(id: &str, name: &str, state: &str, pending: Option<&str>) -> ManagedSessionView {
ManagedSessionView {
id: id.to_string(),
name: name.to_string(),
state: state.to_string(),
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: pending.map(str::to_string),
proposed_default: None,
slot: 0,
deleted: false,
}
}
#[test]
fn render_cli_spawned() {
let r = CommandResult::ManagedSpawned {
id: "uuid-1".into(),
name: "tmpm-red-owl".into(),
state: "Provisioning".into(),
runtime: "claude-code".into(),
attach_cmd: "tmux attach -t '=tmpm-red-owl'".into(),
};
let out = render_cli(&r);
assert_eq!(
out,
"spawned tmpm-red-owl (uuid-1) [Provisioning] runtime=claude-code\n attach: tmux attach -t '=tmpm-red-owl'"
);
}
#[test]
fn render_cli_sessions() {
assert_eq!(
render_cli(&CommandResult::ManagedSessions(vec![])),
"no managed sessions"
);
let r = CommandResult::ManagedSessions(vec![
view("m-1", "alpha", "Running", None),
view("m-2", "bravo", "Stopped", Some("approve deploy?")),
]);
assert_eq!(
render_cli(&r),
"m-1 alpha Running\nm-2 bravo Stopped pending=\"approve deploy?\""
);
}
#[test]
fn render_cli_send_answer_attach() {
assert_eq!(
render_cli(&CommandResult::ManagedSent {
id: "m-1".into(),
tmux_name: "tmpm-red-owl".into()
}),
"sent"
);
assert_eq!(
render_cli(&CommandResult::ManagedAnswered {
id: "m-1".into(),
answer: "yes".into(),
tmux_name: "tmpm-red-owl".into()
}),
"answered"
);
assert_eq!(
render_cli(&CommandResult::ManagedAttachCmd {
id: "m-1".into(),
attach_cmd: "tmux attach -t '=tmpm-red-owl'".into()
}),
"tmux attach -t '=tmpm-red-owl'"
);
}
#[test]
fn render_cli_lifecycle() {
let stop = CommandResult::ManagedLifecycle {
id: "m-1".into(),
name: "alpha".into(),
state: "Stopped".into(),
action: "stopped".into(),
workspace_removed: None,
};
assert_eq!(
render_cli(&stop),
"runtime stopped m-1 (workspace intact; use 'resume' to restart)"
);
let resume = CommandResult::ManagedLifecycle {
id: "m-1".into(),
name: "alpha".into(),
state: "Running".into(),
action: "resumed".into(),
workspace_removed: None,
};
assert_eq!(render_cli(&resume), "resumed alpha (m-1) [Running]");
}
fn decommissioned(verdict: Option<bool>) -> CommandResult {
CommandResult::ManagedLifecycle {
id: "m-1".into(),
name: "alpha".into(),
state: "Decommissioned".into(),
action: "decommissioned".into(),
workspace_removed: verdict,
}
}
#[test]
fn decommission_message_honours_every_verdict() {
assert_eq!(
render_cli(&decommissioned(Some(true))),
"decommissioned m-1 — workspace removed; tombstone record kept"
);
let not_removed = render_cli(&decommissioned(Some(false)));
assert_eq!(
not_removed,
"decommissioned m-1 — tombstone record kept; workspace NOT removed (still on disk)"
);
assert!(
!not_removed.contains("workspace removed"),
"a `workspace_removed: false` verdict must never read as removal: {not_removed}"
);
let unknown = render_cli(&decommissioned(None));
assert_eq!(
unknown,
"decommissioned m-1 — tombstone record kept; workspace outcome not reported by the daemon"
);
assert!(
!unknown.contains("workspace removed"),
"an absent verdict must not be rendered as removal: {unknown}"
);
}
#[tokio::test]
async fn decommission_cli_message_never_claims_removal_when_workspace_remains() {
use std::future::IntoFuture as _;
use trusty_mpm::client::{CommandExecutor, TrustyCommand};
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}-unowned-ws"));
std::fs::create_dir_all(&ws).expect("create seeded workspace");
state
.session_manager()
.await
.create_with_id(
id,
"regression: #5899 decommission workspace verdict".to_string(),
Some(ws.clone()),
None,
Some(ws.clone()),
None,
None,
RuntimeKind::default(),
false,
false,
)
.await
.expect("seed session");
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, api::router(state)).into_future());
let result = CommandExecutor::new(format!("http://{addr}"))
.execute(TrustyCommand::ManagedDecommission {
target: id.to_string(),
})
.await;
let rendered = render_cli(&result);
assert!(
ws.exists(),
"fixture invariant: decommission must NOT delete an unowned workspace \
at {}",
ws.display()
);
assert!(
!rendered.contains("workspace removed"),
"the CLI claimed removal while the workspace is still on disk at {}: \
{rendered}",
ws.display()
);
assert!(
rendered.contains("NOT removed"),
"the operator must be told the workspace survived: {rendered}"
);
}
#[test]
fn render_cli_error() {
assert_eq!(
render_cli(&CommandResult::Error("managed session x not found".into())),
"managed session x not found"
);
}
#[tokio::test]
async fn run_propagates_error_result_as_err() {
let client = reqwest::Client::new();
let action = SessionAction::Send {
id: "nonexistent".into(),
text: "hi".into(),
};
let err = run(
&trusty_mpm::client::DaemonClient::with_client(client.clone(), "http://127.0.0.1:1"),
&action,
)
.await
.expect_err("a CommandResult::Error must propagate as Err, not be swallowed");
assert!(
err.to_string().contains("daemon unreachable"),
"unexpected error: {err}"
);
}
}