use trusty_mpm::client::ManagedSessionSummary;
use super::{
GlobDeleteOutcome, GlobDeleteRequest, GlobSkip, build_plan, confirm_matches_count,
decide_glob_delete, parse_glob_delete, plan_lines,
};
fn session(name: &str, state: &str) -> ManagedSessionSummary {
ManagedSessionSummary {
id: format!("{name}-id"),
name: name.to_string(),
state: state.to_string(),
persisted_state: None,
workspace_path: None,
repo_url: None,
branch: None,
created_at: None,
last_activity_at: None,
pending_decision: None,
proposed_default: None,
source_id: None,
task: None,
cwd: None,
claude_session_id: None,
deliverable_id: None,
pane_id: None,
injection_status: None,
unresumable: false,
stale_assets: false,
stale_assets_unchecked: false,
attached: false,
slot: 1,
deleted: false,
}
}
fn tombstone(name: &str) -> ManagedSessionSummary {
ManagedSessionSummary {
deleted: true,
..session(name, "stopped")
}
}
fn fleet() -> Vec<ManagedSessionSummary> {
vec![
session("tm-test-01", "stopped"),
session("tm-test-02", "errored"),
session("tm-test-03", "active"),
session("tm-apex-02", "stopped"),
tombstone("tm-test-04"),
]
}
fn req(pattern: &str, dry_run: bool) -> GlobDeleteRequest {
GlobDeleteRequest {
pattern: pattern.to_string(),
dry_run,
}
}
#[test]
fn glob_parse_requires_metacharacter() {
assert_eq!(parse_glob_delete("elete"), None);
assert_eq!(parse_glob_delete("tm-test-01"), None);
assert_eq!(parse_glob_delete("2"), None);
assert_eq!(
parse_glob_delete("tm-test-*"),
Some(req("tm-test-*", false))
);
assert_eq!(
parse_glob_delete("tm-test-0?"),
Some(req("tm-test-0?", false))
);
assert_eq!(
parse_glob_delete("tm-test-[12]"),
Some(req("tm-test-[12]", false))
);
}
#[test]
fn glob_parse_rejects_empty() {
assert_eq!(parse_glob_delete(""), None);
assert_eq!(parse_glob_delete(" "), None);
assert_eq!(parse_glob_delete("--dry-run"), None);
}
#[test]
fn glob_parse_strips_dry_run() {
assert_eq!(
parse_glob_delete("tm-test-* --dry-run"),
Some(req("tm-test-*", true))
);
assert_eq!(
parse_glob_delete(" tm-test-* --dry-run "),
Some(req("tm-test-*", true))
);
}
#[test]
fn glob_parse_accepts_leading_dry_run() {
assert_eq!(
parse_glob_delete("--dry-run tm-test-*"),
Some(req("tm-test-*", true))
);
assert_eq!(
parse_glob_delete(" --dry-run tm-test-* "),
Some(req("tm-test-*", true))
);
let parsed = parse_glob_delete("--dry-runner-*").expect("pattern");
assert_eq!(parsed.pattern, "--dry-runner-*");
assert!(!parsed.dry_run);
}
#[test]
fn glob_parse_keeps_inner_dry_run_text() {
let parsed = parse_glob_delete("tm--dry-run-*").expect("pattern");
assert_eq!(parsed.pattern, "tm--dry-run-*");
assert!(!parsed.dry_run);
}
fn plan_for(pattern: &str, sessions: &[ManagedSessionSummary]) -> super::GlobDeletePlan {
plan_for_as(pattern, sessions, None)
}
fn plan_for_as(
pattern: &str,
sessions: &[ManagedSessionSummary],
current: Option<&str>,
) -> super::GlobDeletePlan {
let glob = globset::GlobBuilder::new(pattern)
.literal_separator(false)
.case_insensitive(true)
.build()
.expect("valid glob");
build_plan(sessions, &glob.compile_matcher(), current)
}
#[test]
fn glob_plan_splits_running_from_stopped() {
let fleet = fleet();
let plan = plan_for("tm-test-*", &fleet);
assert_eq!(plan.deletable, vec![0, 1]);
assert_eq!(
plan.skipped,
vec![
(2, GlobSkip::Running("active".to_string())),
(4, GlobSkip::Tombstoned),
]
);
assert_eq!(plan.matched(), 4);
}
#[test]
fn glob_plan_skips_tombstoned_rows() {
let fleet = fleet();
let plan = plan_for("tm-test-04", &fleet);
assert!(plan.deletable.is_empty());
assert_eq!(plan.skipped, vec![(4, GlobSkip::Tombstoned)]);
}
#[test]
fn glob_plan_empty_on_no_match() {
let fleet = fleet();
let plan = plan_for("tm-nothing-*", &fleet);
assert_eq!(plan.matched(), 0);
}
#[test]
fn glob_plan_star_spans_separators() {
let fleet = fleet();
let plan = plan_for("tm-*", &fleet);
assert_eq!(plan.matched(), 5, "every session starts with tm-");
}
#[test]
fn glob_plan_suffix_and_single_char_patterns() {
let fleet = fleet();
assert_eq!(plan_for("*-01", &fleet).deletable, vec![0]);
assert_eq!(plan_for("tm-test-0?", &fleet).matched(), 4);
assert_eq!(plan_for("tm-apex-[0-9]2", &fleet).deletable, vec![3]);
}
#[test]
fn glob_plan_nameless_session_never_matches() {
let sessions = vec![
session("", "stopped"),
session(" ", "stopped"),
session("tm-test-01", "stopped"),
];
let plan = plan_for("*", &sessions);
assert_eq!(
plan.deletable,
vec![2],
"only the named session is deletable under `*`"
);
assert_eq!(
plan.matched(),
1,
"a nameless session must not appear in the match set at all"
);
assert_eq!(plan_for("*", &sessions[..2]).matched(), 0);
}
#[test]
fn glob_plan_excludes_current_session() {
let plan = plan_for_as("tm-test-*", &fleet(), Some("tm-test-01"));
assert!(
!plan.deletable.contains(&0),
"the caller's own session must never be bulk-deleted"
);
assert!(plan.skipped.contains(&(0, GlobSkip::SelfSession)));
assert_eq!(plan.deletable, vec![1]);
}
#[test]
fn glob_plan_not_in_tmux_still_guards_running() {
let plan = plan_for_as("tm-test-*", &fleet(), None);
assert!(
!plan.deletable.contains(&2),
"the active session is kept even with no tmux identity available"
);
assert!(
plan.skipped
.iter()
.any(|(i, r)| *i == 2 && matches!(r, GlobSkip::Running(s) if s == "active"))
);
}
#[test]
fn glob_decide_no_match_is_not_destructive() {
let (outcome, plan) = decide_glob_delete(&fleet(), &req("tm-nothing-*", false), None);
assert_eq!(outcome, GlobDeleteOutcome::NoMatch);
assert!(plan.deletable.is_empty());
assert_ne!(outcome, GlobDeleteOutcome::Confirm);
}
#[test]
fn glob_decide_all_running_is_not_destructive() {
let live = vec![
session("tm-test-01", "active"),
session("tm-test-02", "attached"),
session("tm-test-03", "provisioning"),
];
let (outcome, plan) = decide_glob_delete(&live, &req("tm-test-*", false), None);
assert_eq!(outcome, GlobDeleteOutcome::NothingDeletable);
assert!(
plan.deletable.is_empty(),
"a running session must never enter the bulk delete set"
);
assert_eq!(plan.skipped.len(), 3);
}
#[test]
fn glob_decide_match_all_never_reaches_running_sessions() {
let (outcome, plan) = decide_glob_delete(&fleet(), &req("*", false), None);
assert_eq!(outcome, GlobDeleteOutcome::Confirm);
assert_eq!(plan.matched(), 5, "`*` matches the whole fleet");
assert_eq!(
plan.deletable,
vec![0, 1, 3],
"only stopped/errored rows are deletable under `*`"
);
assert!(
plan.skipped
.iter()
.any(|(i, r)| *i == 2 && matches!(r, GlobSkip::Running(s) if s == "active"))
);
assert!(!confirm_matches_count("y", plan.deletable.len()));
}
#[test]
fn glob_decide_dry_run_never_confirms() {
let (wet, _) = decide_glob_delete(&fleet(), &req("tm-test-*", false), None);
let (dry, plan) = decide_glob_delete(&fleet(), &req("tm-test-*", true), None);
assert_eq!(wet, GlobDeleteOutcome::Confirm);
assert_eq!(dry, GlobDeleteOutcome::DryRun);
assert_ne!(dry, GlobDeleteOutcome::Confirm);
assert_eq!(plan.deletable.len(), 2, "the plan is still computed");
}
#[test]
fn glob_decide_invalid_pattern() {
let (outcome, plan) = decide_glob_delete(&fleet(), &req("tm-test-[", false), None);
assert!(matches!(outcome, GlobDeleteOutcome::InvalidPattern(_)));
assert_ne!(outcome, GlobDeleteOutcome::Confirm);
assert!(plan.deletable.is_empty());
}
#[test]
fn glob_decide_confirm_on_deletable() {
let (outcome, plan) = decide_glob_delete(&fleet(), &req("tm-test-*", false), None);
assert_eq!(outcome, GlobDeleteOutcome::Confirm);
assert_eq!(plan.deletable, vec![0, 1]);
}
#[test]
fn glob_decide_literal_name_pattern() {
let (outcome, plan) = decide_glob_delete(&fleet(), &req("tm-apex-0?", false), None);
assert_eq!(outcome, GlobDeleteOutcome::Confirm);
assert_eq!(plan.deletable, vec![3]);
}
#[test]
fn glob_confirm_requires_exact_count() {
assert!(confirm_matches_count("2", 2));
assert!(confirm_matches_count(" 2 ", 2));
assert!(!confirm_matches_count("1", 2));
assert!(!confirm_matches_count("3", 2));
assert!(!confirm_matches_count("20", 2));
}
#[test]
fn glob_confirm_rejects_yes() {
for line in ["y", "Y", "yes", "YES", "", " ", "all", "force", "n"] {
assert!(
!confirm_matches_count(line, 3),
"'{line}' must not confirm a bulk delete"
);
}
}
#[test]
fn glob_decide_star_never_deletes_nameless_sessions() {
let nameless = vec![session("", "stopped"), session(" ", "errored")];
let (outcome, plan) = decide_glob_delete(&nameless, &req("*", false), None);
assert_eq!(outcome, GlobDeleteOutcome::NoMatch);
assert_ne!(
outcome,
GlobDeleteOutcome::Confirm,
"`*` must not reach the deletion loop via nameless sessions"
);
assert!(plan.deletable.is_empty());
}
#[test]
fn glob_decide_self_only_match_is_not_destructive() {
let mine = vec![session("tm-mine-01", "stopped")];
let (outcome, plan) = decide_glob_delete(&mine, &req("tm-mine-*", false), Some("tm-mine-01"));
assert_eq!(outcome, GlobDeleteOutcome::NothingDeletable);
assert_ne!(outcome, GlobDeleteOutcome::Confirm);
assert!(plan.deletable.is_empty());
}
#[test]
fn glob_report_disambiguates_same_name() {
let dupes = vec![
ManagedSessionSummary {
id: "aaaaaaaa-1111".to_string(),
..session("tm-dogfood", "stopped")
},
ManagedSessionSummary {
id: "bbbbbbbb-2222".to_string(),
..session("tm-dogfood", "stopped")
},
];
let (_, plan) = decide_glob_delete(&dupes, &req("tm-dogfood*", false), None);
assert_eq!(plan.deletable.len(), 2);
let lines = plan_lines(&dupes, &plan);
assert_ne!(
lines[0], lines[1],
"two same-named sessions must not render as identical rows"
);
assert!(lines[0].contains("aaaaaaaa"));
assert!(lines[1].contains("bbbbbbbb"));
}
#[test]
fn glob_report_leads_with_the_tmux_name() {
let fleet = fleet();
let (_, plan) = decide_glob_delete(&fleet, &req("tm-apex-*", false), None);
let lines = plan_lines(&fleet, &plan);
assert_eq!(lines.len(), 1);
assert!(
lines[0].trim_start().starts_with("DELETE tm-apex-02"),
"row must lead with the marker then the tmux name, got: {}",
lines[0]
);
}
#[test]
fn glob_report_marks_delete_rows() {
for pattern in ["tm-test-*", "tm-apex-*", "*"] {
let fleet = fleet();
let (_, plan) = decide_glob_delete(&fleet, &req(pattern, false), None);
let lines = plan_lines(&fleet, &plan);
let marked = lines
.iter()
.filter(|l| l.trim_start().starts_with("DELETE"))
.count();
let kept = lines
.iter()
.filter(|l| l.trim_start().starts_with("keep"))
.count();
assert!(
confirm_matches_count(&marked.to_string(), plan.deletable.len()),
"'{pattern}': counting {marked} DELETE row(s) must be accepted as \
the confirmation, but the prompt expects {}",
plan.deletable.len()
);
assert_eq!(
marked,
plan.deletable.len(),
"'{pattern}': DELETE markers shown must equal the deletable count"
);
assert_eq!(marked + kept, lines.len());
assert_eq!(kept, plan.skipped.len());
assert!(!confirm_matches_count(
&(marked + 1).to_string(),
plan.deletable.len()
));
if marked > 0 {
assert!(!confirm_matches_count(
&(marked - 1).to_string(),
plan.deletable.len()
));
}
}
}
#[test]
fn glob_report_lists_every_match() {
let fleet = fleet();
let (_, plan) = decide_glob_delete(&fleet, &req("tm-test-*", false), None);
let lines = plan_lines(&fleet, &plan);
assert_eq!(
lines.len(),
plan.matched(),
"every matched row is shown, protected ones included"
);
assert!(
lines
.iter()
.any(|l| l.contains("tm-test-01") && l.contains("stopped"))
);
assert!(
lines
.iter()
.any(|l| l.contains("tm-test-03") && l.contains("keep") && l.contains("active"))
);
assert!(
lines
.iter()
.any(|l| l.contains("tm-test-04") && l.contains("already deleted"))
);
}