use std::cell::RefCell;
use std::path::Path;
use super::*;
#[test]
fn route_bare_tm_git_tree_is_guided() {
assert_eq!(
route_bare_tm(true, false, true, true),
BareTmRoute::GuidedProject
);
}
#[test]
fn route_bare_tm_managed_pane_is_guided() {
assert_eq!(
route_bare_tm(false, true, true, true),
BareTmRoute::GuidedProject
);
}
#[test]
fn route_bare_tm_outside_git_tty_is_interactive() {
assert_eq!(
route_bare_tm(false, false, true, true),
BareTmRoute::OutsideGit { interactive: true }
);
}
#[test]
fn route_bare_tm_outside_git_piped_stdin_is_static() {
assert_eq!(
route_bare_tm(false, false, false, true),
BareTmRoute::OutsideGit { interactive: false }
);
}
#[test]
fn route_bare_tm_outside_git_piped_stdout_is_static() {
assert_eq!(
route_bare_tm(false, false, true, false),
BareTmRoute::OutsideGit { interactive: false }
);
}
fn route_for_dir(dir: &Path) -> BareTmRoute {
use crate::commands::guided::{CwdProject, classify_cwd_project};
route_bare_tm(
!matches!(classify_cwd_project(dir), CwdProject::NotGit),
false,
true,
true,
)
}
fn tempdir_with_name(name: &str) -> std::path::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
}
#[test]
fn route_bare_tm_real_git_work_tree_takes_the_guided_dispatch() {
let tmp = tempdir_with_name("trusty_test_outside_git_repo_6666");
let ok = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(&tmp)
.status()
.map(|s| s.success())
.unwrap_or(false);
if !ok {
return;
}
assert_eq!(route_for_dir(&tmp), BareTmRoute::GuidedProject);
}
#[test]
fn route_bare_tm_real_plain_dir_takes_the_outside_git_dispatch() {
let tmp = tempdir_with_name("trusty_test_outside_git_plain_6666");
use crate::commands::guided::{CwdProject, classify_cwd_project};
if !matches!(classify_cwd_project(&tmp), CwdProject::NotGit) {
return;
}
assert_eq!(
route_for_dir(&tmp),
BareTmRoute::OutsideGit { interactive: true }
);
}
#[test]
fn parse_outside_git_choice_n_is_untracked() {
for input in ["n", "N", " n \n", "n\r\n"] {
assert_eq!(
parse_outside_git_choice(input),
OutsideGitChoice::UntrackedSession,
"input {input:?}"
);
}
}
#[test]
fn parse_outside_git_choice_m_is_managed() {
for input in ["m", "M", " m\n"] {
assert_eq!(
parse_outside_git_choice(input),
OutsideGitChoice::ManagedSession,
"input {input:?}"
);
}
}
#[test]
fn parse_outside_git_choice_empty_is_quit() {
for input in ["", "\n", " "] {
assert_eq!(
parse_outside_git_choice(input),
OutsideGitChoice::Quit,
"input {input:?}"
);
}
}
#[test]
fn parse_outside_git_choice_unknown_is_quit() {
for input in ["q", "yes", "1"] {
assert_eq!(
parse_outside_git_choice(input),
OutsideGitChoice::Quit,
"input {input:?}"
);
}
}
#[test]
fn outside_git_untracked_choice_maps_to_sessions_start_argv() {
let argv = choice_argv(&OutsideGitChoice::UntrackedSession, Path::new("/tmp/plain"));
assert_eq!(
argv,
Some(vec![
"sessions".to_string(),
"start".to_string(),
"--dir".to_string(),
"/tmp/plain".to_string(),
])
);
}
#[test]
fn outside_git_managed_choice_maps_to_sessions_new_argv() {
let argv = choice_argv(&OutsideGitChoice::ManagedSession, Path::new("/tmp/plain"));
assert_eq!(
argv,
Some(vec![
"sessions".to_string(),
"new".to_string(),
"/tmp/plain".to_string(),
"--task".to_string(),
String::new(),
])
);
}
#[test]
fn outside_git_quit_choice_maps_to_no_argv() {
assert_eq!(
choice_argv(&OutsideGitChoice::Quit, Path::new("/tmp")),
None
);
}
#[test]
fn outside_git_argv_parses_back_into_the_sessions_group() {
use clap::Parser as _;
for choice in [
OutsideGitChoice::UntrackedSession,
OutsideGitChoice::ManagedSession,
] {
let argv = choice_argv(&choice, Path::new("/tmp/plain")).expect("action choice has argv");
let full = std::iter::once("tm".to_string()).chain(argv.clone());
let cli = crate::cli::Cli::try_parse_from(full)
.unwrap_or_else(|e| panic!("argv {argv:?} must parse: {e}"));
assert!(
matches!(cli.command, Some(crate::cli::Command::Sessions { .. })),
"argv {argv:?} must land in the sessions group"
);
}
}
#[test]
fn outside_git_options_block_names_both_real_commands() {
let block = options_block(Path::new("/tmp/plain"));
assert!(
block.contains("tm sessions start --dir /tmp/plain"),
"block: {block}"
);
assert!(
block.contains("tm sessions new /tmp/plain --task ''"),
"block: {block}"
);
assert!(block.contains("[q] quit"), "block: {block}");
assert!(
block.contains("not inside a git work tree"),
"block: {block}"
);
}
#[test]
fn outside_git_options_block_matches_choice_argv() {
let cwd = Path::new("/tmp/plain");
let block = options_block(cwd);
for choice in [
OutsideGitChoice::UntrackedSession,
OutsideGitChoice::ManagedSession,
] {
for token in choice_argv(&choice, cwd).expect("action choice has argv") {
if token.is_empty() {
continue;
}
assert!(
block.contains(&token),
"token {token:?} missing from block: {block}"
);
}
}
}
#[derive(Default)]
struct MenuSpy {
listed: RefCell<u32>,
read: RefCell<u32>,
ran: RefCell<Vec<Vec<String>>>,
}
#[tokio::test]
async fn outside_git_menu_static_lists_then_prints_options() {
let spy = MenuSpy::default();
let out = run_outside_git_menu(
Path::new("/tmp/plain"),
false,
|| async {
*spy.listed.borrow_mut() += 1;
Ok(())
},
|| {
*spy.read.borrow_mut() += 1;
Ok(String::new())
},
|argv| async {
spy.ran.borrow_mut().push(argv);
Ok(())
},
)
.await;
assert!(out.is_ok(), "static path must exit 0: {out:?}");
assert_eq!(*spy.listed.borrow(), 1, "the listing runs exactly once");
}
#[tokio::test]
async fn outside_git_menu_static_never_reads_or_runs() {
let spy = MenuSpy::default();
run_outside_git_menu(
Path::new("/tmp/plain"),
false,
|| async { Ok(()) },
|| {
*spy.read.borrow_mut() += 1;
Ok("n".to_string())
},
|argv| async {
spy.ran.borrow_mut().push(argv);
Ok(())
},
)
.await
.expect("static path must exit 0");
assert_eq!(*spy.read.borrow(), 0, "no stdin read on a non-TTY");
assert!(spy.ran.borrow().is_empty(), "no action on a non-TTY");
}
#[tokio::test]
async fn outside_git_menu_tty_choice_runs_the_advertised_argv() {
for (key, expected) in [
(
"n\n",
vec!["sessions", "start", "--dir", "/tmp/plain"]
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>(),
),
(
"m\n",
vec!["sessions", "new", "/tmp/plain", "--task", ""]
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>(),
),
] {
let spy = MenuSpy::default();
run_outside_git_menu(
Path::new("/tmp/plain"),
true,
|| async { Ok(()) },
|| Ok(key.to_string()),
|argv| async {
spy.ran.borrow_mut().push(argv);
Ok(())
},
)
.await
.unwrap_or_else(|e| panic!("key {key:?} must succeed: {e}"));
assert_eq!(*spy.ran.borrow(), vec![expected], "key {key:?}");
}
}
#[tokio::test]
async fn outside_git_menu_quit_runs_nothing() {
for key in ["\n", "q\n", ""] {
let spy = MenuSpy::default();
run_outside_git_menu(
Path::new("/tmp/plain"),
true,
|| async { Ok(()) },
|| Ok(key.to_string()),
|argv| async {
spy.ran.borrow_mut().push(argv);
Ok(())
},
)
.await
.unwrap_or_else(|e| panic!("key {key:?} must exit 0: {e}"));
assert!(spy.ran.borrow().is_empty(), "key {key:?} must run nothing");
}
}
#[tokio::test]
async fn outside_git_menu_reports_a_listing_failure_and_still_offers_the_options() {
let spy = MenuSpy::default();
let out = run_outside_git_menu(
Path::new("/tmp/plain"),
true,
|| async { Err(anyhow::anyhow!("daemon unreachable")) },
|| {
*spy.read.borrow_mut() += 1;
Ok("q\n".to_string())
},
|argv| async {
spy.ran.borrow_mut().push(argv);
Ok(())
},
)
.await;
assert!(out.is_ok(), "a dead daemon must not fail the menu: {out:?}");
assert_eq!(*spy.read.borrow(), 1, "the prompt is still offered");
}
#[tokio::test]
async fn outside_git_dispatch_rejects_a_non_session_argv() {
let client = reqwest::Client::new();
let out =
dispatch_session_argv(&client, "http://127.0.0.1:1", vec!["status".to_string()]).await;
assert!(out.is_err(), "a non-sessions argv must be refused");
}