use crate::cli;
use crate::commands::auto_compact_if_needed;
use crate::commands_project::{detect_project_type, ProjectType};
use crate::format::*;
use crate::prompt::*;
use yoagent::agent::Agent;
use yoagent::*;
#[derive(Debug, Clone, PartialEq)]
pub enum DoctorStatus {
Pass,
Fail,
Warn,
}
#[derive(Debug, Clone)]
pub struct DoctorCheck {
pub name: String,
pub status: DoctorStatus,
pub detail: String,
}
pub fn run_doctor_checks(provider: &str, model: &str) -> Vec<DoctorCheck> {
let mut checks = Vec::new();
checks.push(DoctorCheck {
name: "Version".to_string(),
status: DoctorStatus::Pass,
detail: cli::VERSION.to_string(),
});
match std::process::Command::new("git").arg("--version").output() {
Ok(output) if output.status.success() => {
let ver = String::from_utf8_lossy(&output.stdout)
.trim()
.replace("git version ", "")
.to_string();
checks.push(DoctorCheck {
name: "Git".to_string(),
status: DoctorStatus::Pass,
detail: format!("installed ({ver})"),
});
}
_ => {
checks.push(DoctorCheck {
name: "Git".to_string(),
status: DoctorStatus::Fail,
detail: "not found".to_string(),
});
}
}
match std::process::Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.output()
{
Ok(output) if output.status.success() => {
let branch = std::process::Command::new("git")
.args(["branch", "--show-current"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
let b = String::from_utf8_lossy(&o.stdout).trim().to_string();
if b.is_empty() {
None
} else {
Some(b)
}
} else {
None
}
})
.unwrap_or_else(|| "detached".to_string());
checks.push(DoctorCheck {
name: "Git repo".to_string(),
status: DoctorStatus::Pass,
detail: format!("yes (branch: {branch})"),
});
}
_ => {
checks.push(DoctorCheck {
name: "Git repo".to_string(),
status: DoctorStatus::Warn,
detail: "not inside a git repository".to_string(),
});
}
}
checks.push(DoctorCheck {
name: "Provider".to_string(),
status: DoctorStatus::Pass,
detail: provider.to_string(),
});
let env_var = cli::provider_api_key_env(provider);
match env_var {
Some(var_name) => {
if std::env::var(var_name).is_ok() {
checks.push(DoctorCheck {
name: "API key".to_string(),
status: DoctorStatus::Pass,
detail: format!("set ({var_name})"),
});
} else {
checks.push(DoctorCheck {
name: "API key".to_string(),
status: DoctorStatus::Fail,
detail: format!("{var_name} not set"),
});
}
}
None => {
if provider == "ollama" {
checks.push(DoctorCheck {
name: "API key".to_string(),
status: DoctorStatus::Pass,
detail: "not required (ollama)".to_string(),
});
} else {
checks.push(DoctorCheck {
name: "API key".to_string(),
status: DoctorStatus::Warn,
detail: format!("unknown env var for provider '{provider}'"),
});
}
}
}
checks.push(DoctorCheck {
name: "Model".to_string(),
status: DoctorStatus::Pass,
detail: model.to_string(),
});
let mut config_found = Vec::new();
if std::path::Path::new(".yoyo.toml").exists() {
config_found.push(".yoyo.toml");
}
if let Some(user_path) = cli::user_config_path() {
if user_path.exists() {
config_found.push("~/.config/yoyo/config.toml");
}
}
if config_found.is_empty() {
checks.push(DoctorCheck {
name: "Config file".to_string(),
status: DoctorStatus::Warn,
detail: "none found (.yoyo.toml or ~/.config/yoyo/config.toml)".to_string(),
});
} else {
checks.push(DoctorCheck {
name: "Config file".to_string(),
status: DoctorStatus::Pass,
detail: format!("found: {}", config_found.join(", ")),
});
}
let context_files = cli::list_project_context_files();
if context_files.is_empty() {
checks.push(DoctorCheck {
name: "Project context".to_string(),
status: DoctorStatus::Warn,
detail: "no context file (create YOYO.md or run /init)".to_string(),
});
} else {
let descriptions: Vec<String> = context_files
.iter()
.map(|(name, lines)| format!("{name} ({lines} lines)"))
.collect();
checks.push(DoctorCheck {
name: "Project context".to_string(),
status: DoctorStatus::Pass,
detail: descriptions.join(", "),
});
}
match std::process::Command::new("curl").arg("--version").output() {
Ok(output) if output.status.success() => {
checks.push(DoctorCheck {
name: "Curl".to_string(),
status: DoctorStatus::Pass,
detail: "installed (for /docs and /web)".to_string(),
});
}
_ => {
checks.push(DoctorCheck {
name: "Curl".to_string(),
status: DoctorStatus::Warn,
detail: "not found (/docs and /web won't work)".to_string(),
});
}
}
if std::path::Path::new(".yoyo").is_dir() {
checks.push(DoctorCheck {
name: "Memory dir".to_string(),
status: DoctorStatus::Pass,
detail: ".yoyo/ found".to_string(),
});
} else {
checks.push(DoctorCheck {
name: "Memory dir".to_string(),
status: DoctorStatus::Warn,
detail: ".yoyo/ not found (run /remember to create)".to_string(),
});
}
checks
}
pub fn print_doctor_report(checks: &[DoctorCheck]) {
println!("\n {BOLD}🩺 yoyo doctor{RESET}");
println!(" {DIM}─────────────────────────────{RESET}");
for check in checks {
let (icon, color) = match check.status {
DoctorStatus::Pass => ("✓", &GREEN),
DoctorStatus::Fail => ("✗", &RED),
DoctorStatus::Warn => ("⚠", &YELLOW),
};
println!(
" {color}{icon}{RESET} {BOLD}{}{RESET}: {}",
check.name, check.detail
);
}
let passed = checks
.iter()
.filter(|c| c.status == DoctorStatus::Pass)
.count();
let total = checks.len();
let summary_color = if passed == total { &GREEN } else { &YELLOW };
println!("\n {summary_color}{passed}/{total} checks passed{RESET}\n");
}
pub fn handle_doctor(provider: &str, model: &str) {
let checks = run_doctor_checks(provider, model);
print_doctor_report(&checks);
}
#[allow(clippy::vec_init_then_push, unused_mut)]
pub fn health_checks_for_project(
project_type: &ProjectType,
) -> Vec<(&'static str, Vec<&'static str>)> {
match project_type {
ProjectType::Rust => {
let mut checks = vec![("build", vec!["cargo", "build"])];
#[cfg(not(test))]
checks.push(("test", vec!["cargo", "test"]));
checks.push((
"clippy",
vec!["cargo", "clippy", "--all-targets", "--", "-D", "warnings"],
));
checks.push(("fmt", vec!["cargo", "fmt", "--", "--check"]));
checks
}
ProjectType::Node => {
let mut checks: Vec<(&str, Vec<&str>)> = vec![];
#[cfg(not(test))]
checks.push(("test", vec!["npm", "test"]));
checks.push(("lint", vec!["npx", "eslint", "."]));
checks
}
ProjectType::Python => {
let mut checks: Vec<(&str, Vec<&str>)> = vec![];
#[cfg(not(test))]
checks.push(("test", vec!["python", "-m", "pytest"]));
checks.push(("lint", vec!["python", "-m", "flake8", "."]));
checks.push(("typecheck", vec!["python", "-m", "mypy", "."]));
checks
}
ProjectType::Go => {
let mut checks = vec![("build", vec!["go", "build", "./..."])];
#[cfg(not(test))]
checks.push(("test", vec!["go", "test", "./..."]));
checks.push(("vet", vec!["go", "vet", "./..."]));
checks
}
ProjectType::Make => {
#[allow(unused_mut)]
let mut checks: Vec<(&str, Vec<&str>)> = vec![];
#[cfg(not(test))]
checks.push(("test", vec!["make", "test"]));
checks
}
ProjectType::Unknown => vec![],
}
}
pub fn run_health_check_for_project(
project_type: &ProjectType,
) -> Vec<(&'static str, bool, String)> {
let checks = health_checks_for_project(project_type);
let mut results = Vec::new();
for (name, args) in checks {
let start = std::time::Instant::now();
let output = std::process::Command::new(args[0])
.args(&args[1..])
.output();
let elapsed = format_duration(start.elapsed());
match output {
Ok(o) if o.status.success() => {
results.push((name, true, format!("ok ({elapsed})")));
}
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
let first_line = stderr.lines().next().unwrap_or("(unknown error)");
results.push((
name,
false,
format!(
"FAIL ({elapsed}): {}",
truncate_with_ellipsis(first_line, 80)
),
));
}
Err(e) => {
results.push((name, false, format!("ERROR: {e}")));
}
}
}
results
}
pub fn run_health_checks_full_output(
project_type: &ProjectType,
) -> Vec<(&'static str, bool, String)> {
let checks = health_checks_for_project(project_type);
let mut results = Vec::new();
for (name, args) in checks {
let output = std::process::Command::new(args[0])
.args(&args[1..])
.output();
match output {
Ok(o) if o.status.success() => {
results.push((name, true, String::new()));
}
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
let mut full_output = String::new();
if !stdout.is_empty() {
full_output.push_str(&stdout);
}
if !stderr.is_empty() {
if !full_output.is_empty() {
full_output.push('\n');
}
full_output.push_str(&stderr);
}
results.push((name, false, full_output));
}
Err(e) => {
results.push((name, false, format!("ERROR: {e}")));
}
}
}
results
}
pub fn build_fix_prompt(failures: &[(&str, &str)]) -> String {
if failures.is_empty() {
return String::new();
}
let mut prompt = String::from(
"Fix the following build/lint errors in this project. Read the relevant files, understand the errors, and apply fixes:\n\n",
);
for (name, output) in failures {
prompt.push_str(&format!("## {name} errors:\n```\n{output}\n```\n\n"));
}
prompt.push_str(
"After fixing, run the failing checks again to verify. Fix any remaining issues.",
);
prompt
}
pub fn handle_health() {
let project_type = detect_project_type(&std::env::current_dir().unwrap_or_default());
println!("{DIM} Detected project: {project_type}{RESET}");
if project_type == ProjectType::Unknown {
println!(
"{DIM} No recognized project found. Looked for: Cargo.toml, package.json, pyproject.toml, setup.py, go.mod, Makefile{RESET}\n"
);
return;
}
println!("{DIM} Running health checks...{RESET}");
let results = run_health_check_for_project(&project_type);
if results.is_empty() {
println!("{DIM} No checks configured for {project_type}{RESET}\n");
return;
}
let all_passed = results.iter().all(|(_, passed, _)| *passed);
for (name, passed, detail) in &results {
let icon = if *passed {
format!("{GREEN}✓{RESET}")
} else {
format!("{RED}✗{RESET}")
};
println!(" {icon} {name}: {detail}");
}
if all_passed {
println!("\n{GREEN} All checks passed ✓{RESET}\n");
} else {
println!("\n{RED} Some checks failed ✗{RESET}\n");
}
}
pub async fn handle_fix(
agent: &mut Agent,
session_total: &mut Usage,
model: &str,
) -> Option<String> {
let project_type = detect_project_type(&std::env::current_dir().unwrap_or_default());
if project_type == ProjectType::Unknown {
println!(
"{DIM} No recognized project found. Looked for: Cargo.toml, package.json, pyproject.toml, setup.py, go.mod, Makefile{RESET}\n"
);
return None;
}
println!("{DIM} Detected project: {project_type}{RESET}");
println!("{DIM} Running health checks...{RESET}");
let results = run_health_checks_full_output(&project_type);
if results.is_empty() {
println!("{DIM} No checks configured for {project_type}{RESET}\n");
return None;
}
for (name, passed, _) in &results {
let icon = if *passed {
format!("{GREEN}✓{RESET}")
} else {
format!("{RED}✗{RESET}")
};
let status = if *passed { "ok" } else { "FAIL" };
println!(" {icon} {name}: {status}");
}
let failures: Vec<(&str, &str)> = results
.iter()
.filter(|(_, passed, _)| !passed)
.map(|(name, _, output)| (*name, output.as_str()))
.collect();
if failures.is_empty() {
println!("\n{GREEN} All checks passed — nothing to fix ✓{RESET}\n");
return None;
}
let fail_count = failures.len();
println!("\n{YELLOW} Sending {fail_count} failure(s) to AI for fixing...{RESET}\n");
let fix_prompt = build_fix_prompt(&failures);
run_prompt(agent, &fix_prompt, session_total, model).await;
auto_compact_if_needed(agent);
Some(fix_prompt)
}
pub fn test_command_for_project(
project_type: &ProjectType,
) -> Option<(&'static str, Vec<&'static str>)> {
match project_type {
ProjectType::Rust => Some(("cargo test", vec!["cargo", "test"])),
ProjectType::Node => Some(("npm test", vec!["npm", "test"])),
ProjectType::Python => Some(("python -m pytest", vec!["python", "-m", "pytest"])),
ProjectType::Go => Some(("go test ./...", vec!["go", "test", "./..."])),
ProjectType::Make => Some(("make test", vec!["make", "test"])),
ProjectType::Unknown => None,
}
}
pub fn handle_test() -> Option<String> {
let project_type = detect_project_type(&std::env::current_dir().unwrap_or_default());
println!("{DIM} Detected project: {project_type}{RESET}");
if project_type == ProjectType::Unknown {
println!(
"{DIM} No recognized project found. Looked for: Cargo.toml, package.json, pyproject.toml, setup.py, go.mod, Makefile{RESET}\n"
);
return None;
}
let (label, args) = match test_command_for_project(&project_type) {
Some(cmd) => cmd,
None => {
println!("{DIM} No test command configured for {project_type}{RESET}\n");
return None;
}
};
println!("{DIM} Running: {label}...{RESET}");
let start = std::time::Instant::now();
let output = std::process::Command::new(args[0])
.args(&args[1..])
.output();
let elapsed = format_duration(start.elapsed());
match output {
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
if !stdout.is_empty() {
print!("{stdout}");
}
if !stderr.is_empty() {
eprint!("{stderr}");
}
if o.status.success() {
println!("\n{GREEN} ✓ Tests passed ({elapsed}){RESET}\n");
Some(format!("Tests passed ({elapsed}): {label}"))
} else {
let code = o.status.code().unwrap_or(-1);
println!("\n{RED} ✗ Tests failed (exit {code}, {elapsed}){RESET}\n");
let mut summary = format!("Tests FAILED (exit {code}, {elapsed}): {label}");
let error_text = if !stderr.is_empty() {
stderr.to_string()
} else {
stdout.to_string()
};
let lines: Vec<&str> = error_text.lines().collect();
let preview_lines = if lines.len() > 20 {
&lines[lines.len() - 20..]
} else {
&lines
};
summary.push_str("\n\nLast output:\n");
for line in preview_lines {
summary.push_str(line);
summary.push('\n');
}
Some(summary)
}
}
Err(e) => {
eprintln!("{RED} ✗ Failed to run {label}: {e}{RESET}\n");
Some(format!("Failed to run {label}: {e}"))
}
}
}
pub fn lint_command_for_project(
project_type: &ProjectType,
) -> Option<(&'static str, Vec<&'static str>)> {
match project_type {
ProjectType::Rust => Some((
"cargo clippy --all-targets -- -D warnings",
vec!["cargo", "clippy", "--all-targets", "--", "-D", "warnings"],
)),
ProjectType::Node => Some(("npx eslint .", vec!["npx", "eslint", "."])),
ProjectType::Python => Some(("ruff check .", vec!["ruff", "check", "."])),
ProjectType::Go => Some(("golangci-lint run", vec!["golangci-lint", "run"])),
ProjectType::Make | ProjectType::Unknown => None,
}
}
pub fn handle_lint() -> Option<String> {
let project_type = detect_project_type(&std::env::current_dir().unwrap_or_default());
println!("{DIM} Detected project: {project_type}{RESET}");
if project_type == ProjectType::Unknown {
println!(
"{DIM} No recognized project found. Looked for: Cargo.toml, package.json, pyproject.toml, setup.py, go.mod, Makefile{RESET}\n"
);
return None;
}
let (label, args) = match lint_command_for_project(&project_type) {
Some(cmd) => cmd,
None => {
println!("{DIM} No lint command configured for {project_type}{RESET}\n");
return None;
}
};
println!("{DIM} Running: {label}...{RESET}");
let start = std::time::Instant::now();
let output = std::process::Command::new(args[0])
.args(&args[1..])
.output();
let elapsed = format_duration(start.elapsed());
match output {
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
if !stdout.is_empty() {
print!("{stdout}");
}
if !stderr.is_empty() {
eprint!("{stderr}");
}
if o.status.success() {
println!("\n{GREEN} ✓ Lint passed ({elapsed}){RESET}\n");
Some(format!("Lint passed ({elapsed}): {label}"))
} else {
let code = o.status.code().unwrap_or(-1);
println!("\n{RED} ✗ Lint failed (exit {code}, {elapsed}){RESET}\n");
let mut summary = format!("Lint FAILED (exit {code}, {elapsed}): {label}");
let error_text = if !stderr.is_empty() {
stderr.to_string()
} else {
stdout.to_string()
};
let lines: Vec<&str> = error_text.lines().collect();
let preview_lines = if lines.len() > 20 {
&lines[lines.len() - 20..]
} else {
&lines
};
summary.push_str("\n\nLast output:\n");
for line in preview_lines {
summary.push_str(line);
summary.push('\n');
}
Some(summary)
}
}
Err(e) => {
eprintln!("{RED} ✗ Failed to run {label}: {e}{RESET}\n");
Some(format!("Failed to run {label}: {e}"))
}
}
}
pub fn detect_test_command() -> Option<String> {
let dir = std::env::current_dir().unwrap_or_default();
let project_type = detect_project_type(&dir);
test_command_for_project(&project_type).map(|(label, _args)| label.to_string())
}
pub const WATCH_SUBCOMMANDS: &[&str] = &["off", "status"];
pub fn handle_watch(input: &str) {
let arg = input.strip_prefix("/watch").unwrap_or("").trim();
match arg {
"" => {
match detect_test_command() {
Some(cmd) => {
crate::prompt::set_watch_command(&cmd);
println!(
"{GREEN} 👀 Watch mode ON — will run `{cmd}` after agent edits{RESET}\n"
);
}
None => {
println!("{DIM} No test command detected. Specify one:{RESET}");
println!("{DIM} /watch cargo test{RESET}");
println!("{DIM} /watch npm test{RESET}\n");
}
}
}
"off" => {
crate::prompt::clear_watch_command();
println!("{DIM} 👀 Watch mode OFF{RESET}\n");
}
"status" => match crate::prompt::get_watch_command() {
Some(cmd) => {
println!("{DIM} 👀 Watch mode: ON{RESET}");
println!("{DIM} Command: `{cmd}`{RESET}\n");
}
None => {
println!("{DIM} 👀 Watch mode: OFF{RESET}\n");
}
},
custom_cmd => {
crate::prompt::set_watch_command(custom_cmd);
println!(
"{GREEN} 👀 Watch mode ON — will run `{custom_cmd}` after agent edits{RESET}\n"
);
}
}
}
pub fn build_project_tree(max_depth: usize) -> String {
let files = match crate::git::run_git(&["ls-files"]) {
Ok(text) => {
let mut files: Vec<String> = text
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
.collect();
files.sort();
files
}
Err(_) => return "(not a git repository — /tree requires git)".to_string(),
};
if files.is_empty() {
return "(no tracked files)".to_string();
}
format_tree_from_paths(&files, max_depth)
}
pub fn format_tree_from_paths(paths: &[String], max_depth: usize) -> String {
use std::collections::BTreeSet;
let mut output = String::new();
let mut printed_dirs: BTreeSet<String> = BTreeSet::new();
for path in paths {
let parts: Vec<&str> = path.split('/').collect();
let depth = parts.len() - 1;
for level in 0..parts.len().saturating_sub(1).min(max_depth) {
let dir_path: String = parts[..=level].join("/");
let dir_key = format!("{}/", dir_path);
if printed_dirs.insert(dir_key) {
let indent = " ".repeat(level);
let dir_name = parts[level];
output.push_str(&format!("{indent}{dir_name}/\n"));
}
}
if depth <= max_depth {
let indent = " ".repeat(depth.min(max_depth));
let file_name = parts.last().unwrap_or(&"");
output.push_str(&format!("{indent}{file_name}\n"));
}
}
if output.ends_with('\n') {
output.truncate(output.len() - 1);
}
output
}
pub fn handle_tree(input: &str) {
let arg = input.strip_prefix("/tree").unwrap_or("").trim();
let max_depth = if arg.is_empty() {
3
} else {
match arg.parse::<usize>() {
Ok(d) => d,
Err(_) => {
println!("{DIM} usage: /tree [depth] (default depth: 3){RESET}\n");
return;
}
}
};
let tree = build_project_tree(max_depth);
println!("{DIM}{tree}{RESET}\n");
}
pub fn run_shell_command(cmd: &str) {
let start = std::time::Instant::now();
let output = std::process::Command::new("sh").args(["-c", cmd]).output();
let elapsed = format_duration(start.elapsed());
match output {
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
if !stdout.is_empty() {
print!("{stdout}");
}
if !stderr.is_empty() {
eprint!("{RED}{stderr}{RESET}");
}
let code = o.status.code().unwrap_or(-1);
if code == 0 {
println!("{DIM} ✓ exit {code} ({elapsed}){RESET}\n");
} else {
println!("{RED} ✗ exit {code} ({elapsed}){RESET}\n");
}
}
Err(e) => {
eprintln!("{RED} error running command: {e}{RESET}\n");
}
}
}
pub fn handle_run(input: &str) {
let cmd = if input.starts_with("/run ") {
input.trim_start_matches("/run ").trim()
} else if input.starts_with('!') && input.len() > 1 {
input[1..].trim()
} else {
""
};
if cmd.is_empty() {
println!("{DIM} usage: /run <command> or !<command>{RESET}\n");
} else {
run_shell_command(cmd);
}
}
pub fn handle_run_usage() {
println!("{DIM} usage: /run <command> or !<command>");
println!(" Runs a shell command directly (no AI, no tokens).{RESET}\n");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_rust() {
let cmd = test_command_for_project(&ProjectType::Rust);
assert!(cmd.is_some());
let (label, _) = cmd.unwrap();
assert_eq!(label, "cargo test");
}
#[test]
fn test_command_unknown() {
assert!(test_command_for_project(&ProjectType::Unknown).is_none());
}
#[test]
fn lint_command_rust() {
let cmd = lint_command_for_project(&ProjectType::Rust);
assert!(cmd.is_some());
assert!(cmd.unwrap().0.contains("clippy"));
}
#[test]
fn lint_command_make_none() {
assert!(lint_command_for_project(&ProjectType::Make).is_none());
}
#[test]
fn lint_command_unknown_none() {
assert!(lint_command_for_project(&ProjectType::Unknown).is_none());
}
#[test]
fn health_checks_rust_has_build() {
let checks = health_checks_for_project(&ProjectType::Rust);
assert!(checks.iter().any(|(name, _)| *name == "build"));
}
#[test]
fn health_checks_unknown_empty() {
let checks = health_checks_for_project(&ProjectType::Unknown);
assert!(checks.is_empty());
}
#[test]
fn build_fix_prompt_empty() {
let prompt = build_fix_prompt(&[]);
assert!(prompt.is_empty());
}
#[test]
fn build_fix_prompt_with_failures() {
let failures = vec![("build", "error[E0308]: mismatched types")];
let prompt = build_fix_prompt(&failures);
assert!(prompt.contains("build errors"));
assert!(prompt.contains("E0308"));
assert!(prompt.contains("Fix"));
}
#[test]
fn build_fix_prompt_multiple_failures() {
let failures = vec![
("build", "build error output"),
("clippy", "clippy warning output"),
];
let prompt = build_fix_prompt(&failures);
assert!(prompt.contains("## build errors"));
assert!(prompt.contains("## clippy errors"));
}
#[test]
fn format_tree_basic() {
let paths = vec![
"src/main.rs".to_string(),
"src/lib.rs".to_string(),
"Cargo.toml".to_string(),
];
let tree = format_tree_from_paths(&paths, 3);
assert!(tree.contains("src/"));
assert!(tree.contains("main.rs"));
assert!(tree.contains("lib.rs"));
assert!(tree.contains("Cargo.toml"));
}
#[test]
fn format_tree_depth_limit() {
let paths = vec!["a/b/c/d/e.txt".to_string()];
let tree_shallow = format_tree_from_paths(&paths, 1);
assert!(tree_shallow.contains("a/"));
assert!(!tree_shallow.contains("e.txt"));
}
#[test]
fn format_tree_empty() {
let paths: Vec<String> = vec![];
let tree = format_tree_from_paths(&paths, 3);
assert!(tree.is_empty());
}
#[test]
fn format_tree_root_files() {
let paths = vec!["README.md".to_string()];
let tree = format_tree_from_paths(&paths, 3);
assert!(tree.contains("README.md"));
}
}