use std::path::PathBuf;
use std::sync::Mutex;
use crate::commands::first_run::needs_first_run_clone;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
use crate::commands::guided::{
PickerDecision, derive_project, fallback_protected, github_host, is_github_remote,
nested_managed_match, non_github_refusal_message, parse_picker_choice, print_non_tty_hint,
print_project_context, tty_gate,
};
use crate::commands::guided_launch::spawn_progress_message;
use crate::commands::guided_resume::{ResumeAction, is_zombie, needs_restart, plan_resume};
use crate::commands::managed::{filter_live_sessions, is_live_session_state};
#[test]
fn guided_picker_bare_enter_no_sessions_launches_new() {
assert_eq!(parse_picker_choice("", 0, false), PickerDecision::LaunchNew);
assert_eq!(
parse_picker_choice(" \t", 0, false),
PickerDecision::LaunchNew
);
}
#[test]
fn guided_picker_bare_enter_live_session_resumes_first() {
assert_eq!(parse_picker_choice("", 1, false), PickerDecision::Resume(0));
assert_eq!(
parse_picker_choice(" ", 3, false),
PickerDecision::Resume(0)
);
}
#[test]
fn guided_picker_bare_enter_stopped_session_requires_confirm() {
assert_eq!(
parse_picker_choice("", 1, true),
PickerDecision::ConfirmRestart(0)
);
assert_eq!(
parse_picker_choice(" ", 3, true),
PickerDecision::ConfirmRestart(0)
);
}
#[test]
fn guided_picker_q_returns_quit() {
assert_eq!(parse_picker_choice("q", 0, false), PickerDecision::Quit);
assert_eq!(parse_picker_choice("q", 3, true), PickerDecision::Quit);
}
#[test]
fn guided_picker_q_uppercase_returns_quit() {
assert_eq!(parse_picker_choice("Q", 2, false), PickerDecision::Quit);
assert_eq!(parse_picker_choice("Q\n", 0, false), PickerDecision::Quit);
}
#[test]
fn guided_picker_numeric_valid_resumes() {
assert_eq!(parse_picker_choice("1", 1, true), PickerDecision::Resume(0));
assert_eq!(parse_picker_choice("1", 3, true), PickerDecision::Resume(0));
assert_eq!(
parse_picker_choice("2", 3, false),
PickerDecision::Resume(1)
);
assert_eq!(
parse_picker_choice("3", 3, false),
PickerDecision::Resume(2)
);
assert_eq!(
parse_picker_choice("2\n", 3, false),
PickerDecision::Resume(1)
);
}
#[test]
fn guided_picker_numeric_launch_new() {
assert_eq!(
parse_picker_choice("1", 0, false),
PickerDecision::LaunchNew
);
assert_eq!(
parse_picker_choice("4", 3, false),
PickerDecision::LaunchNew
);
}
#[test]
fn guided_picker_out_of_range_unrecognised() {
assert_eq!(
parse_picker_choice("5", 3, false),
PickerDecision::Unrecognised
);
assert_eq!(
parse_picker_choice("100", 1, false),
PickerDecision::Unrecognised
);
assert_eq!(
parse_picker_choice("0", 3, false),
PickerDecision::Unrecognised
);
}
#[test]
fn guided_picker_non_numeric_unrecognised() {
assert_eq!(
parse_picker_choice("abc", 2, false),
PickerDecision::Unrecognised
);
assert_eq!(
parse_picker_choice("exit", 0, false),
PickerDecision::Unrecognised
);
assert_eq!(
parse_picker_choice("1a", 3, false),
PickerDecision::Unrecognised
);
}
#[test]
fn guided_non_tty_gate_returns_false_skips_stdin() {
let result = tty_gate(false, "owner/repo", &PathBuf::from("/ws/owner/repo"), &[]);
assert!(
!result,
"non-TTY gate must return false (no picker) for empty sessions"
);
}
#[test]
fn guided_tty_gate_returns_true_for_tty() {
let result = tty_gate(true, "owner/repo", &PathBuf::from("/ws/owner/repo"), &[]);
assert!(
result,
"TTY gate must return true so caller runs the picker"
);
}
#[test]
fn guided_non_tty_gate_returns_false_with_sessions() {
let sessions = vec![make_session("tm-api-1", "running", None)];
let result = tty_gate(false, "owner/repo", &PathBuf::from("/ws"), &sessions);
assert!(
!result,
"non-TTY gate must return false regardless of session count"
);
}
#[test]
fn guided_derive_project_returns_none_for_non_git_dir() {
let tmp = std::env::temp_dir();
let non_git = tmp.join("trusty_test_non_git_dir_1705");
std::fs::create_dir_all(&non_git).ok();
let result = derive_project(&non_git);
assert!(
result.is_none(),
"expected None for non-git dir, got {result:?}"
);
}
#[test]
fn guided_derive_project_rejects_non_github_remote() {
let tmp = tempdir_with_name("trusty_test_non_github_remote_1705");
let ok = git_init_quiet(&tmp);
if !ok {
return; }
git_remote_add(&tmp, "https://gitlab.com/owner/repo.git");
let result = derive_project(&tmp);
assert!(
result.is_none(),
"expected None for non-GitHub remote (gitlab), got {result:?}"
);
}
#[test]
fn guided_derive_project_accepts_github_https_remote() {
let tmp = tempdir_with_name("trusty_test_github_https_1705");
let ok = git_init_with_commit(&tmp);
if !ok {
return;
}
git_remote_add(&tmp, "https://github.com/owner/my-repo.git");
let result = derive_project(&tmp);
match result {
Some((source_id, workspace, git_root)) => {
assert_eq!(source_id, "owner/my-repo");
assert!(
!workspace.as_os_str().is_empty(),
"workspace must be non-empty"
);
assert_ne!(workspace, tmp, "workspace must differ from live checkout");
let canonical_root = git_root.canonicalize().unwrap_or(git_root);
let canonical_tmp = tmp.canonicalize().unwrap_or(tmp.clone());
assert_eq!(
canonical_root, canonical_tmp,
"git_root must be the repo root"
);
}
None => panic!("expected Some for GitHub HTTPS remote, got None"),
}
}
#[test]
fn guided_derive_project_accepts_github_ssh_remote() {
let tmp = tempdir_with_name("trusty_test_github_ssh_1705");
let ok = git_init_with_commit(&tmp);
if !ok {
return;
}
git_remote_add(&tmp, "git@github.com:owner/my-repo.git");
let result = derive_project(&tmp);
match result {
Some((source_id, _workspace, _git_root)) => {
assert_eq!(source_id, "owner/my-repo");
}
None => panic!("expected Some for GitHub SSH remote, got None"),
}
}
#[test]
fn guided_derive_project_returns_some_from_subdir() {
let tmp = tempdir_with_name("trusty_test_subdir_1705");
let ok = git_init_with_commit(&tmp);
if !ok {
return;
}
git_remote_add(&tmp, "https://github.com/owner/my-repo.git");
let subdir = tmp.join("src").join("lib");
std::fs::create_dir_all(&subdir).unwrap();
let result = derive_project(&subdir);
match result {
Some((source_id, _workspace, git_root)) => {
assert_eq!(source_id, "owner/my-repo");
let canonical_root = git_root.canonicalize().unwrap_or(git_root);
let canonical_tmp = tmp.canonicalize().unwrap_or(tmp.clone());
assert_eq!(
canonical_root, canonical_tmp,
"git_root from subdir must be repo root, not the nested dir"
);
}
None => panic!("expected Some when calling derive_project from a subdir"),
}
}
#[test]
fn guided_print_project_context_does_not_panic_no_sessions() {
print_project_context(
"owner/repo",
&PathBuf::from("/home/user/repos/owner/repo"),
&[],
);
}
#[test]
fn guided_print_project_context_does_not_panic_with_sessions() {
let sessions = vec![make_session(
"tm-frontend-1",
"running",
Some("2026-06-25T12:00:00Z"),
)];
print_project_context(
"owner/repo",
&PathBuf::from("/home/user/repos/owner/repo"),
&sessions,
);
}
#[test]
fn guided_print_non_tty_hint_does_not_panic_no_sessions() {
print_non_tty_hint("owner/repo", &[]);
}
#[test]
fn guided_print_non_tty_hint_does_not_panic_with_sessions() {
let sessions = vec![make_session("tm-api-2", "stopped", None)];
print_non_tty_hint("owner/repo", &sessions);
}
#[tokio::test]
async fn guided_fallback_non_git_dir_calls_launch_path() {
let tmp = tempdir_with_name("trusty_test_fallback_nongit_1705");
let client = reqwest::Client::new();
let result = fallback_protected(&client, "http://127.0.0.1:19999", &tmp).await;
if let Err(e) = result {
let msg = e.to_string();
assert!(
!msg.contains("live git checkout"),
"non-git dir should NOT trigger live-checkout guard; got: {msg}"
);
}
}
fn tempdir_with_name(name: &str) -> PathBuf {
let tmp = std::env::temp_dir().join(name);
if tmp.exists() {
std::fs::remove_dir_all(&tmp).ok();
}
std::fs::create_dir_all(&tmp).expect("create temp dir");
tmp
}
fn git_init_quiet(dir: &PathBuf) -> bool {
std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(dir)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn git_init_with_commit(dir: &PathBuf) -> bool {
if !git_init_quiet(dir) {
return false;
}
for (k, v) in [("user.email", "test@example.com"), ("user.name", "Test")] {
std::process::Command::new("git")
.args(["config", k, v])
.current_dir(dir)
.status()
.ok();
}
std::process::Command::new("git")
.args(["commit", "--allow-empty", "-m", "init", "-q"])
.current_dir(dir)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn git_remote_add(dir: &PathBuf, url: &str) {
std::process::Command::new("git")
.args(["remote", "add", "origin", url])
.current_dir(dir)
.status()
.ok();
}
fn make_session(
name: &str,
state: &str,
last_activity_at: Option<&str>,
) -> trusty_mpm::client::ManagedSessionSummary {
trusty_mpm::client::ManagedSessionSummary {
id: format!("{name}-id"),
name: name.to_string(),
state: state.to_string(),
workspace_path: None,
repo_url: None,
branch: None,
created_at: None,
last_activity_at: last_activity_at.map(str::to_owned),
pending_decision: None,
proposed_default: None,
source_id: None,
task: None,
cwd: None,
claude_session_id: None,
}
}
#[test]
fn guided_resume_needs_restart_stopped() {
assert!(needs_restart("stopped"), "stopped state must need restart");
}
#[test]
fn guided_resume_needs_restart_errored() {
assert!(needs_restart("errored"), "errored state must need restart");
}
#[test]
fn guided_resume_no_restart_active() {
assert!(
!needs_restart("active"),
"active state must not need daemon restart"
);
}
#[test]
fn guided_resume_no_restart_provisioning() {
assert!(
!needs_restart("provisioning"),
"provisioning must not need restart"
);
}
#[test]
fn guided_resume_no_restart_decommissioned() {
assert!(
!needs_restart("decommissioned"),
"decommissioned must not need restart"
);
}
#[test]
fn guided_resume_is_zombie_active_no_tmux() {
assert!(
is_zombie("active", false),
"active + no tmux must be detected as zombie"
);
}
#[test]
fn guided_resume_is_zombie_provisioning_no_tmux() {
assert!(
is_zombie("provisioning", false),
"provisioning + no tmux must be detected as zombie"
);
}
#[test]
fn guided_resume_not_zombie_stopped_no_tmux() {
assert!(
!is_zombie("stopped", false),
"stopped + no tmux is a restart case, not a zombie"
);
}
#[test]
fn guided_resume_not_zombie_errored_no_tmux() {
assert!(
!is_zombie("errored", false),
"errored + no tmux is a restart case, not a zombie"
);
}
#[test]
fn guided_resume_not_zombie_active_with_tmux() {
assert!(
!is_zombie("active", true),
"active + live tmux is the normal attach path, not a zombie"
);
}
#[test]
fn guided_resume_plan_active_live_tmux_attaches() {
assert_eq!(
plan_resume("active", true),
ResumeAction::Attach,
"active + live tmux must attach directly"
);
}
#[test]
fn guided_resume_plan_stopped_restarts() {
assert_eq!(
plan_resume("stopped", false),
ResumeAction::Restart,
"stopped must select the plain Restart path"
);
}
#[test]
fn guided_resume_plan_errored_restarts() {
assert_eq!(
plan_resume("errored", false),
ResumeAction::Restart,
"errored must select the plain Restart path"
);
}
#[test]
fn guided_resume_plan_active_no_tmux_reconciles_then_restarts() {
assert_eq!(
plan_resume("active", false),
ResumeAction::ReconcileThenRestart,
"active + no tmux must reconcile (auto-stop) then restart"
);
}
#[test]
fn guided_resume_plan_provisioning_no_tmux_reconciles_then_restarts() {
assert_eq!(
plan_resume("provisioning", false),
ResumeAction::ReconcileThenRestart,
"provisioning + no tmux must reconcile then restart"
);
}
#[test]
fn guided_resume_plan_stopped_with_stale_tmux_still_restarts() {
assert_eq!(
plan_resume("stopped", true),
ResumeAction::Restart,
"stopped + stale live tmux must still take the Restart path, not reconcile"
);
}
#[test]
fn is_github_remote_accepts_github_com_ssh() {
assert!(
is_github_remote("git@github.com:o/r.git"),
"github.com SSH must be recognised as GitHub"
);
}
#[test]
fn is_github_remote_accepts_github_com_https() {
assert!(
is_github_remote("https://github.com/o/r.git"),
"github.com HTTPS must be recognised as GitHub"
);
}
#[test]
fn is_github_remote_accepts_github_hyphen_alias() {
assert!(
is_github_remote("git@github-duetto:duettoresearch/aria.git"),
"github-<alias> SSH remote must be recognised as GitHub"
);
}
#[test]
fn is_github_remote_accepts_github_alias_ssh_url_style() {
assert!(
is_github_remote("ssh://git@github-work/o/r"),
"ssh:// github-<alias> must be recognised as GitHub"
);
}
#[test]
fn is_github_remote_accepts_github_underscore_alias() {
assert!(
is_github_remote("git@github_personal:user/repo.git"),
"github_<alias> SSH remote must be recognised as GitHub"
);
}
#[test]
fn is_github_remote_rejects_gitlab() {
assert!(
!is_github_remote("git@gitlab.com:o/r.git"),
"gitlab.com must NOT be recognised as GitHub"
);
}
#[test]
fn is_github_remote_rejects_bitbucket() {
assert!(
!is_github_remote("https://bitbucket.org/o/r"),
"bitbucket.org must NOT be recognised as GitHub"
);
}
#[test]
fn is_github_remote_rejects_githubusercontent() {
assert!(
!is_github_remote("https://raw.githubusercontent.com/o/r/file"),
"githubusercontent.com must NOT be recognised as GitHub"
);
}
#[test]
fn is_github_remote_accepts_github_com_https_with_port() {
assert!(
is_github_remote("https://github.com:443/o/r.git"),
"https://github.com:443/… must be recognised as GitHub (port stripped)"
);
}
#[test]
fn is_github_remote_rejects_gitea_with_github_in_path() {
assert!(
!is_github_remote("https://gitea.example.com/mirrors/github-fork.git"),
"gitea host with github in path must NOT match"
);
}
#[test]
fn github_host_extracts_scp_style() {
assert_eq!(
github_host("git@github-duetto:duettoresearch/aria.git"),
"github-duetto"
);
assert_eq!(github_host("git@github.com:owner/repo.git"), "github.com");
}
#[test]
fn github_host_extracts_https() {
assert_eq!(
github_host("https://github.com/owner/repo.git"),
"github.com"
);
assert_eq!(github_host("https://gitlab.com/o/r"), "gitlab.com");
}
#[test]
fn github_host_extracts_ssh_url_with_user() {
assert_eq!(github_host("ssh://git@github-work/o/r"), "github-work");
}
#[test]
fn guided_derive_project_accepts_github_ssh_alias() {
let tmp = tempdir_with_name("trusty_test_github_alias_derive_1705");
let ok = git_init_with_commit(&tmp);
if !ok {
return;
}
git_remote_add(&tmp, "git@github-duetto:duettoresearch/aria.git");
let result = derive_project(&tmp);
match result {
Some((source_id, _workspace, _git_root)) => {
assert_eq!(
source_id, "duettoresearch/aria",
"source_id must be parsed correctly from alias remote"
);
}
None => panic!("expected Some for GitHub SSH alias remote, got None"),
}
}
#[tokio::test]
#[serial_test::serial]
async fn guided_fallback_does_not_refuse_github_ssh_alias_remote() {
let dir = tempdir_with_name("trusty_test_github_alias_fallback_1705");
let ok = std::process::Command::new("git")
.arg("init")
.current_dir(&dir)
.status()
.map(|s| s.success())
.unwrap_or(false);
if !ok {
eprintln!(
"guided_fallback_does_not_refuse_github_ssh_alias_remote: git unavailable, skipping"
);
return;
}
let _ = std::process::Command::new("git")
.args([
"remote",
"add",
"origin",
"git@github-test-fixture-alias-nonexistent:acmetest/repo-fixture.git",
])
.current_dir(&dir)
.status();
let repos_root = tempfile::tempdir().unwrap();
let repos_root_key = trusty_mpm::daemon::managed_routes::inproject::REPOS_ROOT_ENV;
let prev = std::env::var(repos_root_key).ok();
unsafe { std::env::set_var(repos_root_key, repos_root.path()) };
let client = reqwest::Client::new();
let result = fallback_protected(&client, "http://127.0.0.1:1", &dir).await;
unsafe {
match prev {
Some(v) => std::env::set_var(repos_root_key, v),
None => std::env::remove_var(repos_root_key),
}
}
assert!(
result.is_err(),
"fallback must Err (daemon unreachable / clone fails)"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
!err_msg.contains("auto-managed clones require a GitHub remote"),
"SSH alias remote must NOT trigger GitHub-remote refusal; got: {err_msg}"
);
assert!(
!dir.join("CLAUDE.md").exists(),
"CLAUDE.md must NOT appear in the live checkout"
);
assert!(
!dir.join(".mcp.json").exists(),
".mcp.json must NOT appear in the live checkout"
);
}
#[test]
fn needs_first_run_clone_returns_none_for_non_dir() {
assert!(needs_first_run_clone("https://github.com/owner/repo.git").is_none());
assert!(needs_first_run_clone("/nonexistent/path/that/does/not/exist").is_none());
assert!(needs_first_run_clone("").is_none());
}
#[test]
#[serial_test::serial]
fn needs_first_run_clone_returns_none_when_clone_exists() {
use std::process::Command;
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return; }
git(&[
"remote",
"add",
"origin",
"git@github.com:owner/already-cloned.git",
]);
let repos_env_key = trusty_mpm::daemon::managed_routes::inproject::REPOS_ROOT_ENV;
let tmp_repos = tempfile::TempDir::new().unwrap();
let base = tmp_repos.path().join("owner").join("already-cloned");
std::fs::create_dir_all(base.join(".git")).unwrap();
let prev = std::env::var(repos_env_key).ok();
let result = {
let _env_guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::set_var(repos_env_key, tmp_repos.path()) };
let r = needs_first_run_clone(&dir.to_string_lossy());
unsafe {
match prev {
Some(v) => std::env::set_var(repos_env_key, v),
None => std::env::remove_var(repos_env_key),
}
}
r
};
assert!(
result.is_none(),
"base clone exists → must return None (no first-run message)"
);
}
#[test]
#[serial_test::serial]
fn needs_first_run_clone_returns_some_when_no_clone() {
use std::process::Command;
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return;
}
git(&[
"remote",
"add",
"origin",
"git@github.com:myorg/my-new-project.git",
]);
let repos_env_key = trusty_mpm::daemon::managed_routes::inproject::REPOS_ROOT_ENV;
let tmp_repos = tempfile::TempDir::new().unwrap();
let prev = std::env::var(repos_env_key).ok();
let result = {
let _env_guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::set_var(repos_env_key, tmp_repos.path()) };
let r = needs_first_run_clone(&dir.to_string_lossy());
unsafe {
match prev {
Some(v) => std::env::set_var(repos_env_key, v),
None => std::env::remove_var(repos_env_key),
}
}
r
};
let (proj, _path) = result.expect("must return Some for a first-run scenario");
assert_eq!(proj, "myorg/my-new-project");
}
#[test]
fn spawn_progress_message_first_run() {
let path = PathBuf::from("/home/user/trusty-mpm-projects/owner/repo");
let msg = spawn_progress_message(Some(&("owner/repo".to_string(), path.clone())));
assert!(msg.contains("owner/repo"));
assert!(msg.contains(&path.display().to_string()));
assert!(msg.contains("first run"));
}
#[test]
fn spawn_progress_message_reuse() {
let msg = spawn_progress_message(None);
assert_eq!(msg, "tm: launching new session…");
}
#[test]
fn guided_non_github_refusal_message_does_not_mention_daemon_or_start() {
let msg = non_github_refusal_message("git@gitlab.com:org/repo.git");
assert!(
!msg.contains("daemon unreachable"),
"refusal message must NOT blame the daemon: {msg}"
);
assert!(
!msg.contains("tm start"),
"refusal message must NOT instruct to start the daemon: {msg}"
);
assert!(
!msg.contains("Start the daemon"),
"refusal message must NOT mention starting the daemon: {msg}"
);
}
#[test]
fn guided_non_github_refusal_message_explains_github_only_policy() {
let msg = non_github_refusal_message("https://gitea.example.com/org/repo.git");
assert!(
msg.contains("GitHub"),
"refusal must name GitHub as the auto-management requirement: {msg}"
);
assert!(
msg.to_lowercase().contains("auto-manag"),
"refusal must mention auto-management scope: {msg}"
);
}
#[test]
fn guided_non_github_refusal_message_includes_detected_remote() {
let remote = "git@gitlab.com:myorg/myrepo.git";
let msg = non_github_refusal_message(remote);
assert!(
msg.contains(remote),
"refusal must echo the detected remote ({remote}): {msg}"
);
}
#[test]
fn guided_non_github_refusal_message_reassures_live_checkout_untouched() {
let msg = non_github_refusal_message("https://bitbucket.org/org/repo.git");
assert!(
msg.contains("live checkout"),
"refusal must reassure that the live checkout was not touched: {msg}"
);
assert!(
msg.contains("not touched"),
"refusal must use the phrase 'not touched': {msg}"
);
}
#[test]
fn picker_filter_live_state_excludes_decommissioned() {
assert!(
!is_live_session_state("decommissioned"),
"decommissioned must be excluded from default view"
);
assert!(
is_live_session_state("active"),
"active must be included in default view"
);
assert!(
is_live_session_state("stopped"),
"stopped must be included in default view"
);
assert!(
is_live_session_state("errored"),
"errored must be included in default view"
);
assert!(
is_live_session_state("provisioning"),
"provisioning must be included in default view"
);
}
#[test]
fn picker_filter_excludes_decommissioned_keeps_active() {
let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
serde_json::from_value(serde_json::json!([
{ "id": "a1", "name": "sess-active", "state": "active" },
{ "id": "b2", "name": "sess-dead-1", "state": "decommissioned" },
{ "id": "c3", "name": "sess-stopped", "state": "stopped" },
{ "id": "d4", "name": "sess-dead-2", "state": "decommissioned" },
{ "id": "e5", "name": "sess-provisioning", "state": "provisioning" },
]))
.expect("test data must deserialize");
let filtered = filter_live_sessions(sessions);
assert_eq!(
filtered.len(),
3,
"filter must keep exactly 3 live sessions (active, stopped, provisioning)"
);
assert!(
filtered.iter().any(|s| s.state == "active"),
"active session must survive filter"
);
assert!(
filtered.iter().any(|s| s.state == "stopped"),
"stopped session must survive filter"
);
assert!(
filtered.iter().any(|s| s.state == "provisioning"),
"provisioning session must survive filter"
);
assert!(
!filtered.iter().any(|s| s.state == "decommissioned"),
"decommissioned tombstones must be excluded"
);
}
#[test]
fn picker_filter_all_live_sessions_unchanged() {
let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
serde_json::from_value(serde_json::json!([
{ "id": "x1", "name": "sess-a", "state": "active" },
{ "id": "x2", "name": "sess-b", "state": "stopped" },
{ "id": "x3", "name": "sess-c", "state": "errored" },
]))
.expect("test data must deserialize");
let filtered = filter_live_sessions(sessions);
assert_eq!(
filtered.len(),
3,
"all-live input must pass through unchanged (3 sessions)"
);
}
#[test]
fn picker_filter_all_decommissioned_returns_empty() {
let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
serde_json::from_value(serde_json::json!([
{ "id": "z1", "name": "old-1", "state": "decommissioned" },
{ "id": "z2", "name": "old-2", "state": "decommissioned" },
]))
.expect("test data must deserialize");
let filtered = filter_live_sessions(sessions);
assert!(
filtered.is_empty(),
"all-decommissioned input must produce empty list"
);
}
#[test]
fn daily_banner_two_panel_version_in_title_bar_not_content() {
use crate::formatters::banner::two_panel::{render_two_panel_banner, strip_ansi};
use crate::formatters::info_box::{DaemonInfo, WelcomeData};
colored::control::set_override(false);
let data = WelcomeData {
project: "owner/repo".to_string(),
workspace: "/home/alice/trusty-mpm-projects/owner/repo".to_string(),
user: "alice".to_string(),
reconnecting: false,
session_name: String::new(),
daemon: DaemonInfo::default(),
recent_commits: vec![],
memory_status: "(not detected)".to_string(),
search_status: "(not detected)".to_string(),
review_status: "(not detected)".to_string(),
};
let version = env!("CARGO_PKG_VERSION");
let banner =
render_two_panel_banner(&data, 120, false).expect("120-col terminal must produce banner");
let bare = strip_ansi(&banner);
let count = bare.matches(version).count();
assert_eq!(
count, 1,
"version must appear exactly once (title bar only); found {count}"
);
let first = bare.lines().next().unwrap_or("");
assert!(
first.starts_with('╭'),
"title bar must start with ╭: {first:?}"
);
assert!(
first.contains(version),
"title bar must contain the version: {first:?}"
);
for (i, line) in bare.lines().enumerate().skip(1) {
let inner = line.trim_start_matches('│').trim_end_matches('│').trim();
assert!(
!inner.starts_with(&format!("trusty-mpm v{version}")),
"content row {i} must not carry the version line: {line:?}"
);
}
colored::control::unset_override();
}
#[test]
fn nested_managed_match_by_session_name() {
let sessions = vec![make_session("tm-proj-01", "active", None)];
let matched = nested_managed_match(Some("tm-proj-01"), None, &sessions);
assert_eq!(matched.map(|s| s.name.as_str()), Some("tm-proj-01"));
}
#[test]
fn nested_managed_match_by_env_id() {
let sessions = vec![make_session("tm-proj-01", "active", None)];
let matched = nested_managed_match(None, Some("tm-proj-01-id"), &sessions);
assert_eq!(matched.map(|s| s.name.as_str()), Some("tm-proj-01"));
}
#[test]
fn nested_managed_match_none_when_no_match() {
let sessions = vec![make_session("tm-proj-01", "active", None)];
let matched = nested_managed_match(Some("some-other-session"), Some("unrelated-id"), &sessions);
assert!(matched.is_none());
}
#[test]
fn nested_managed_match_none_when_both_inputs_absent() {
let sessions = vec![make_session("tm-proj-01", "active", None)];
let matched = nested_managed_match(None, None, &sessions);
assert!(matched.is_none());
}
#[test]
fn nested_managed_match_finds_record_missing_from_source_id_filtered_list() {
let mut orphaned = make_session("tm-orphan-02", "active", None);
orphaned.source_id = None;
let sessions = vec![orphaned];
let matched = nested_managed_match(Some("tm-orphan-02"), None, &sessions);
assert!(
matched.is_some(),
"must match by session name regardless of source_id"
);
}