use std::{fs, sync as path_std_sync};
use crate::completion::{
CommandCompletion, CompletionData, CompletionRule, CompletionRules, build_candidates,
build_candidates_with_home, build_candidates_with_home_and_cwd, build_candidates_with_rules,
};
#[test]
fn dotslash_token_triggers_filesystem_candidates() {
let cwd = tempfile::tempdir().expect("tempdir");
fs::write(cwd.path().join("tau-cli-term-dotslash.txt"), "").expect("write test file");
let buffer = "./tau-cli-term-dotslash";
let candidates = build_candidates_with_home_and_cwd(
&[CommandCompletion::new(":whatever", "")],
&CompletionData::new(),
buffer,
buffer.len(),
None,
cwd.path(),
);
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].label, "./tau-cli-term-dotslash.txt");
assert_eq!(candidates[0].description, "file");
assert_eq!(candidates[0].replacement, "./tau-cli-term-dotslash.txt");
}
#[test]
fn home_relative_token_previews_tilde_and_accepts_absolute_home_path() {
let home = tempfile::tempdir().expect("tempdir");
fs::write(home.path().join("alpha.txt"), "").expect("write alpha");
fs::write(home.path().join("beta.txt"), "").expect("write beta");
let buffer = r#"open ~/a and "~/literal""#;
let cursor = "open ~/a".len();
let cands = build_candidates_with_home(
&[CommandCompletion::new(":whatever", "")],
&CompletionData::new(),
buffer,
cursor,
Some(home.path()),
);
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].label, "~/alpha.txt");
assert_eq!(cands[0].replacement, r#"open ~/alpha.txt and "~/literal""#);
assert_eq!(cands[0].cursor, "open ~/alpha.txt".len());
let acceptance = cands[0].acceptance.as_ref().expect("home acceptance");
assert_eq!(
acceptance.replacement,
format!(
r#"open {} and "~/literal""#,
home.path().join("alpha.txt").display()
)
);
assert_eq!(
acceptance.cursor,
"open ".len() + home.path().join("alpha.txt").to_string_lossy().len()
);
}
#[test]
fn home_relative_token_without_home_has_no_completion() {
let buffer = "open ~/a";
assert!(
build_candidates_with_home(
&[CommandCompletion::new(":whatever", "")],
&CompletionData::new(),
buffer,
buffer.len(),
None,
)
.is_empty()
);
}
#[cfg(unix)]
#[test]
fn home_relative_token_with_non_utf8_home_uses_lossy_absolute_acceptance() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let parent = tempfile::tempdir().expect("tempdir");
let home = parent.path().join(OsString::from_vec(vec![b'h', 0x80]));
fs::create_dir(&home).expect("create non-UTF-8 home");
fs::write(home.join("alpha.txt"), "").expect("write alpha");
let buffer = "open ~/a";
let candidates = build_candidates_with_home(
&[CommandCompletion::new(":whatever", "")],
&CompletionData::new(),
buffer,
buffer.len(),
Some(&home),
);
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].replacement, "open ~/alpha.txt");
assert_eq!(
candidates[0]
.acceptance
.as_ref()
.expect("home acceptance")
.replacement,
format!("open {}", home.join("alpha.txt").to_string_lossy())
);
}
#[test]
fn filesystem_directory_candidates_include_trailing_slash() {
let home = tempfile::tempdir().expect("tempdir");
fs::create_dir(home.path().join("alpha-dir")).expect("mkdir alpha-dir");
fs::write(home.path().join("alpha.txt"), "").expect("write alpha file");
let buffer = "open ~/alpha";
let cursor = buffer.len();
let cands = build_candidates_with_home(
&[CommandCompletion::new(":whatever", "")],
&CompletionData::new(),
buffer,
cursor,
Some(home.path()),
);
let dir = cands
.iter()
.find(|cand| cand.description == "directory")
.expect("directory candidate");
assert_eq!(dir.label, "~/alpha-dir/");
assert_eq!(dir.replacement, "open ~/alpha-dir/");
let file = cands
.iter()
.find(|cand| cand.description == "file")
.expect("file candidate");
assert_eq!(file.label, "~/alpha.txt");
assert_eq!(file.replacement, "open ~/alpha.txt");
}
#[test]
fn command_buffer_does_not_route_to_filesystem() {
let cands = build_candidates(
&[CommandCompletion::new(":model", "Switch model")],
&CompletionData::new(),
":mod",
":mod".len(),
);
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].replacement, ":model");
}
#[test]
fn leading_absolute_path_completes_from_the_filesystem() {
let directory = tempfile::tempdir().expect("tempdir");
fs::write(directory.path().join("alpha.txt"), "").expect("write alpha");
let buffer = format!("{}/a", directory.path().display());
let candidates = build_candidates(
&[CommandCompletion::new(":model", "Switch model")],
&CompletionData::new(),
&buffer,
buffer.len(),
);
assert_eq!(candidates.len(), 1);
assert_eq!(
candidates[0].replacement,
format!("{}/alpha.txt", directory.path().display())
);
assert!(!candidates[0].label.starts_with(':'));
}
#[test]
fn configured_colon_rules_cannot_shadow_command_mode() {
let rules = CompletionRules::new(vec![
CompletionRule::parse(":", "complete_path").expect("path rule"),
CompletionRule::parse(":set", "complete_with_command printf hostile")
.expect("command rule"),
]);
let data = CompletionData::new();
let root = build_candidates_with_rules(
&[CommandCompletion::new(":model", "Switch model")],
&data,
&rules,
":mod",
":mod".len(),
);
assert_eq!(root.len(), 1);
assert_eq!(root[0].replacement, ":model");
assert!(
build_candidates_with_rules(
&[CommandCompletion::new(":set", "Set UI state")],
&data,
&rules,
":set /tmp",
":set /tmp".len(),
)
.is_empty()
);
assert!(
rules
.command_for_exact_token(":set", ":set".len())
.is_none()
);
}
#[test]
fn literal_colon_escape_does_not_enter_command_completion() {
assert!(
build_candidates(
&[CommandCompletion::new(":model", "Switch model")],
&CompletionData::new(),
"::model",
"::model".len(),
)
.is_empty()
);
}
#[test]
fn at_token_completes_agent_mentions_in_prompt_text() {
let data = CompletionData::new();
data.set_agent_mention_completer(path_std_sync::Arc::new(|args| {
assert_eq!(args, ["wo"]);
vec![crate::completion::CompletionItem::new("worker", "agent")]
}));
let buffer = "ask @wo for help";
let cursor = "ask @wo".len();
let cands = build_candidates(
&[CommandCompletion::new(":model", "Switch model")],
&data,
buffer,
cursor,
);
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].label, "worker");
assert_eq!(cands[0].description, "agent");
assert_eq!(cands[0].replacement, "ask @worker for help");
}
#[test]
fn ampersand_token_completes_running_session_ids() {
let data = CompletionData::new();
data.set_session_completer(path_std_sync::Arc::new(|args| {
assert_eq!(args, ["roj"]);
vec![crate::completion::CompletionItem::new(
"project-main",
"/work/project",
)]
}));
let buffer = "ask &roj about this";
let cursor = "ask &roj".len();
let candidates = build_candidates(&[], &data, buffer, cursor);
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].label, "project-main");
assert_eq!(candidates[0].description, "/work/project");
assert_eq!(candidates[0].replacement, "ask &project-main about this");
assert_eq!(candidates[0].cursor, "ask &project-main".len());
}
#[test]
fn complete_sessions_rule_parses_and_is_built_in() {
assert!(matches!(
CompletionRule::parse("&", "complete_sessions").map(|rule| rule.kind),
Some(crate::completion::CompletionRuleKind::Sessions)
));
let data = CompletionData::new();
data.set_session_completer(path_std_sync::Arc::new(|_| {
vec![crate::completion::CompletionItem::plain("session-one")]
}));
assert_eq!(build_candidates(&[], &data, "&", 1).len(), 1);
}
#[test]
fn session_completion_rejects_embedded_tokens_and_remote_agent_suffixes() {
let data = CompletionData::new();
data.set_session_completer(path_std_sync::Arc::new(|_| {
vec![crate::completion::CompletionItem::plain("project-main")]
}));
assert!(build_candidates(&[], &data, "x&proj", "x&proj".len()).is_empty());
assert!(build_candidates(&[], &data, "&proj/@agent", "&proj".len()).is_empty());
assert!(build_candidates(&[], &data, "&proj/@agent", "&proj/@agent".len()).is_empty());
}
#[test]
fn command_mode_does_not_offer_session_completion() {
let data = CompletionData::new();
data.set_session_completer(path_std_sync::Arc::new(|_| {
vec![crate::completion::CompletionItem::plain("project-main")]
}));
assert!(build_candidates(&[], &data, ":anything &proj", 15).is_empty());
}
#[test]
fn leading_whitespace_command_preserves_prefix() {
let cands = build_candidates(
&[CommandCompletion::new(":model", "Switch model")],
&CompletionData::new(),
" :mod",
" :mod".len(),
);
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].replacement, " :model");
}
#[test]
fn configured_actions_preserve_surrounding_prompt_text() {
let rules = CompletionRules::new(vec![
CompletionRule::parse("#", "complete_actions").expect("rule parses"),
]);
let cands = build_candidates_with_rules(
&[CommandCompletion::new(":model", "Switch model")],
&CompletionData::new(),
&rules,
"ask #mod now",
"ask #mod".len(),
);
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].replacement, "ask #model now");
}
#[test]
fn command_completion_requires_whole_token_and_loses_to_leading_command_tokens() {
let rules = CompletionRules::new(vec![
CompletionRule::parse("#/", "complete_with_command fzf").expect("rule parses"),
CompletionRule::parse(":", "complete_with_command fzf").expect("rule parses"),
]);
assert!(rules.command_for_exact_token("#/foo", "#/".len()).is_none());
assert!(rules.command_for_exact_token(":", ":".len()).is_none());
assert!(
rules
.command_for_exact_token("ask #/", "ask #/".len())
.is_some()
);
}
#[test]
fn non_slash_non_path_buffer_returns_nothing() {
let cands = build_candidates(
&[CommandCompletion::new(":model", "Switch model")],
&CompletionData::new(),
"hello",
"hello".len(),
);
assert!(cands.is_empty());
}
#[test]
fn parent_traversal_token_is_recognised() {
let parent = tempfile::tempdir().expect("tempdir");
let cwd = parent.path().join("child");
fs::create_dir(&cwd).expect("create child directory");
fs::write(parent.path().join("tau-cli-term-parent.txt"), "").expect("write test file");
let buffer = "../tau-cli-term-parent";
let candidates = build_candidates_with_home_and_cwd(
&[CommandCompletion::new(":whatever", "")],
&CompletionData::new(),
buffer,
buffer.len(),
None,
&cwd,
);
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].label, "../tau-cli-term-parent.txt");
assert_eq!(candidates[0].description, "file");
assert_eq!(candidates[0].replacement, "../tau-cli-term-parent.txt");
}