use trusty_mpm::client::{
CommandExecutor, CommandResult, ManagedSessionSummary, TrustyCommand, resolve_target,
};
use crate::cli::SessionAction;
fn executor(client: &reqwest::Client, url: &str) -> CommandExecutor {
CommandExecutor::with_client(client.clone(), url.to_string())
}
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 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,
} => match action.as_str() {
"stopped" => {
format!("runtime stopped {id} (workspace intact; use 'resume' to restart)")
}
"resumed" => format!("resumed {name} ({id}) [{state}]"),
"decommissioned" => {
format!("decommissioned {id} (workspace removed; tombstone record kept)")
}
other => format!("{other} {id} ({name}) [{state}]"),
},
CommandResult::Error(msg) => msg.clone(),
other => format!("unexpected result: {other:?}"),
}
}
pub(crate) async fn resolve_managed_summary(
client: &reqwest::Client,
url: &str,
id_or_name: &str,
) -> Option<ManagedSessionSummary> {
let sessions = executor(client, url)
.client()
.list_managed_sessions()
.await
.ok()?;
resolve_target(&sessions, id_or_name).cloned()
}
pub(crate) async fn resolve_managed_match(
client: &reqwest::Client,
url: &str,
id_or_name: &str,
) -> Option<String> {
resolve_managed_summary(client, url, id_or_name)
.await
.map(|s| s.id)
}
pub(crate) async fn resolve_project_session_id(
client: &reqwest::Client,
url: &str,
id_or_name: &str,
) -> anyhow::Result<Option<String>> {
let rows = executor(client, url).client().sessions().await?;
Ok(resolve_target(&rows, id_or_name).map(|r| r.id.0.to_string()))
}
pub(crate) async fn run(
client: &reqwest::Client,
url: &str,
action: &SessionAction,
) -> anyhow::Result<bool> {
let Some(cmd) = to_command(action) else {
return Ok(false);
};
let result = executor(client, url).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() }),
Some(TrustyCommand::ManagedDecommission { .. })
));
assert!(matches!(
to_command(&SessionAction::Ls {
terms: vec![],
json: false,
source_id: None,
current: false,
all: 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(),
};
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(),
};
assert_eq!(render_cli(&resume), "resumed alpha (m-1) [Running]");
let decom = CommandResult::ManagedLifecycle {
id: "m-1".into(),
name: "alpha".into(),
state: "Decommissioned".into(),
action: "decommissioned".into(),
};
assert_eq!(
render_cli(&decom),
"decommissioned m-1 (workspace removed; tombstone record kept)"
);
}
#[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(&client, "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}"
);
}
}