mod input_processing {
use std::fs;
use tempfile::TempDir;
fn process_input(input: &str, project_root: &std::path::Path) -> (String, Vec<String>) {
let mut prompt_parts = Vec::new();
let mut files_loaded = Vec::new();
for token in input.split_whitespace() {
if let Some(raw_path) = token.strip_prefix('@') {
if raw_path.is_empty() {
prompt_parts.push(token.to_string());
continue;
}
let full_path = project_root.join(raw_path);
if full_path.is_file() {
files_loaded.push(raw_path.to_string());
} else {
prompt_parts.push(token.to_string());
}
} else {
prompt_parts.push(token.to_string());
}
}
let prompt = prompt_parts.join(" ");
let prompt = if prompt.trim().is_empty() && !files_loaded.is_empty() {
"Describe and explain the attached files.".to_string()
} else {
prompt
};
(prompt, files_loaded)
}
#[test]
fn test_at_file_reference_resolved() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("main.rs"), "fn main() {}").unwrap();
let (prompt, files) = process_input("explain @main.rs", dir.path());
assert_eq!(prompt, "explain");
assert_eq!(files, vec!["main.rs"]);
}
#[test]
fn test_at_file_missing_stays_in_prompt() {
let dir = TempDir::new().unwrap();
let (prompt, files) = process_input("explain @nonexistent.rs", dir.path());
assert!(prompt.contains("@nonexistent.rs"));
assert!(files.is_empty());
}
#[test]
fn test_at_file_only_gets_default_prompt() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("code.py"), "x = 1").unwrap();
let (prompt, files) = process_input("@code.py", dir.path());
assert_eq!(prompt, "Describe and explain the attached files.");
assert_eq!(files.len(), 1);
}
#[test]
fn test_multiple_at_files() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("a.rs"), "a").unwrap();
fs::write(dir.path().join("b.rs"), "b").unwrap();
let (prompt, files) = process_input("compare @a.rs @b.rs", dir.path());
assert_eq!(prompt, "compare");
assert_eq!(files.len(), 2);
}
#[test]
fn test_bare_at_sign_ignored() {
let dir = TempDir::new().unwrap();
let (prompt, files) = process_input("email me @ noon", dir.path());
assert_eq!(prompt, "email me @ noon");
assert!(files.is_empty());
}
#[test]
fn test_no_at_references() {
let dir = TempDir::new().unwrap();
let (prompt, files) = process_input("just a question", dir.path());
assert_eq!(prompt, "just a question");
assert!(files.is_empty());
}
}
mod completions {
const EXPECTED_COMMANDS: &[&str] = &[
"/agent",
"/compact",
"/cost",
"/diff",
"/help",
"/memory",
"/model",
"/provider",
"/sessions",
"/skills",
];
const REMOVED_COMMANDS: &[&str] = &["/key", "/transcript"];
#[test]
fn test_expected_commands_present() {
assert_eq!(EXPECTED_COMMANDS.len(), 10, "Expected 10 slash commands");
for cmd in EXPECTED_COMMANDS {
assert!(
EXPECTED_COMMANDS.contains(cmd),
"Expected command {cmd} missing from completions"
);
}
}
#[test]
fn test_removed_commands_absent() {
for cmd in REMOVED_COMMANDS {
assert!(
!EXPECTED_COMMANDS.contains(cmd),
"Removed command {cmd} should not be in completions"
);
}
}
}
mod display_regression {
const KNOWN_TOOLS: &[(&str, &str)] = &[
("Read", "Read"),
("List", "List"),
("Write", "Write"),
("Edit", "Edit"),
("Delete", "Delete"),
("Grep", "Search"),
("Glob", "Glob"),
("Bash", "Shell"),
("WebFetch", "Fetch"),
("MemoryRead", "Memory"),
("MemoryWrite", "Memory"),
("ShareReasoning", "Tool"),
("InvokeAgent", "Agent"),
("ListAgents", "Tool"),
("TodoWrite", "Todo"),
("TodoRead", "Todo"),
("AstAnalysis", "AST"),
];
fn tool_label(name: &str) -> &'static str {
match name {
"Read" => "Read",
"List" => "List",
"Write" => "Write",
"Edit" => "Edit",
"Delete" => "Delete",
"Grep" => "Search",
"Glob" => "Glob",
"Bash" => "Shell",
"WebFetch" => "Fetch",
"MemoryRead" | "MemoryWrite" => "Memory",
"InvokeAgent" => "Agent",
"TodoWrite" | "TodoRead" => "Todo",
"AstAnalysis" => "AST",
_ => "Tool",
}
}
#[test]
fn test_all_tools_have_banners() {
for (tool, expected_label) in KNOWN_TOOLS {
assert_eq!(
tool_label(tool),
*expected_label,
"Tool '{tool}' should have label '{expected_label}'"
);
}
}
#[test]
fn test_unknown_tool_gets_generic_banner() {
assert_eq!(tool_label("some_new_tool"), "Tool");
}
#[test]
fn test_tool_count() {
assert_eq!(
KNOWN_TOOLS.len(),
17,
"Expected 17 known tools (update this test when adding tools)"
);
}
}
mod event_loop_structure {
#[test]
fn draw_called_before_idle_select_in_event_loop() {
let source = include_str!("../src/tui_context/mod.rs");
let fn_start = source
.find("async fn run_event_loop")
.expect("run_event_loop function not found in tui_context.rs");
let body = &source[fn_start..];
let select_marker = "self.crossterm_events.next()";
let select_pos = body
.find(select_marker)
.expect("idle select! with crossterm_events.next() not found in run_event_loop");
let before_select = &body[..select_pos];
let has_active_draw = before_select.lines().any(|line| {
let trimmed = line.trim();
!trimmed.starts_with("//") && trimmed.contains("self.draw()")
});
assert!(
has_active_draw,
"self.draw() must be called (uncommented) before the idle tokio::select! in \
run_event_loop. Without it the viewport never redraws and input appears \
frozen (v0.1.11 bug)."
);
}
}
mod provider_key_flow {
#[test]
fn test_same_provider_should_prompt_for_key() {
let current_provider = "openai";
let selected_provider = "openai";
let is_same = current_provider == selected_provider;
let is_local = selected_provider == "lmstudio";
let key_exists = true;
let should_prompt = !is_local && (is_same || !key_exists);
assert!(should_prompt);
}
#[test]
fn test_new_provider_without_key_prompts() {
let is_same = false;
let is_local = false;
let key_exists = false;
let should_prompt = !is_local && (is_same || !key_exists);
assert!(should_prompt);
}
#[test]
fn test_new_provider_with_key_skips_prompt() {
let is_same = false;
let is_local = false;
let key_exists = true;
let should_prompt = !is_local && (is_same || !key_exists);
assert!(!should_prompt);
}
#[test]
fn test_lmstudio_never_prompts_for_key() {
let is_local = true;
let should_prompt = !is_local;
assert!(!should_prompt);
}
}