use assert_cmd::Command;
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use serde_json::Value;
use std::path::PathBuf;
use tempfile::TempDir;
fn snapshot_exists(root: &std::path::Path) -> bool {
loctree::snapshot::Snapshot::exists(root)
}
fn fixtures_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn loctree() -> Command {
cargo_bin_cmd!("loctree")
}
mod cli_basics {
use super::*;
#[test]
fn shows_help() {
loctree()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("loctree"))
.stdout(predicate::str::contains("slice"));
}
#[test]
fn shows_version() {
loctree()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn shows_full_help() {
loctree()
.arg("--help-full")
.assert()
.success()
.stdout(predicate::str::contains("--sarif").or(predicate::str::contains("sarif")))
.stdout(predicate::str::contains("dead").or(predicate::str::contains("cycles")));
}
#[test]
fn global_fresh_flag_does_not_break_subcommands() {
loctree()
.args(["--fresh", "query", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("loct query"));
}
}
mod scan_mode {
use super::*;
#[test]
fn scans_typescript_project() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.assert()
.success()
.stderr(predicate::str::contains("ts").or(predicate::str::contains("Scanned")));
}
#[test]
fn creates_snapshot() {
let temp = TempDir::new().unwrap();
let fixture = fixtures_path().join("simple_ts");
copy_dir_all(&fixture, temp.path()).unwrap();
loctree().current_dir(temp.path()).assert().success();
assert!(snapshot_exists(temp.path()));
}
#[test]
fn respects_gitignore_flag() {
let fixture = fixtures_path().join("simple_ts");
loctree().current_dir(&fixture).arg("-g").assert().success();
}
}
mod slice_mode {
use super::*;
fn ensure_snapshot(fixture: &std::path::Path) {
loctree().current_dir(fixture).assert().success();
}
#[test]
fn slices_single_file() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["slice", "src/index.ts"])
.assert()
.success()
.stdout(predicate::str::contains("Core"))
.stdout(predicate::str::contains("index.ts"));
}
#[test]
fn slice_rescan_flag_triggers_rescan() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["slice", "src/index.ts", "--rescan"])
.write_stdin("")
.assert()
.success()
.stderr(predicate::str::contains("Rescanning"));
}
#[test]
fn slices_with_deps() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["slice", "src/index.ts"])
.assert()
.success()
.stdout(predicate::str::contains("Deps"))
.stdout(predicate::str::contains("greeting.ts"))
.stdout(predicate::str::contains("date.ts"));
}
#[test]
fn slices_with_consumers() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["slice", "src/utils/greeting.ts", "--consumers"])
.assert()
.success()
.stdout(predicate::str::contains("Consumers"))
.stdout(predicate::str::contains("index.ts"));
}
#[test]
fn slices_json_output() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["slice", "src/index.ts", "--json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""core""#))
.stdout(predicate::str::contains(r#""deps""#));
}
#[test]
fn slice_file_not_found() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["slice", "nonexistent.ts"])
.assert()
.failure();
}
}
mod analyzer_mode {
use super::*;
#[test]
fn detects_circular_imports() {
let fixture = fixtures_path().join("circular_imports");
loctree()
.current_dir(&fixture)
.args(["-A", "--circular"])
.assert()
.success()
.stdout(predicate::str::contains("Circular"));
}
#[test]
fn detects_dead_exports() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["-A", "--dead"])
.assert()
.success()
.stdout(
predicate::str::contains("deadFunction")
.or(predicate::str::contains("DEAD_CONSTANT")),
);
}
#[test]
fn lists_entrypoints() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["-A", "--entrypoints"])
.assert()
.success()
.stdout(
predicate::str::is_empty()
.not()
.or(predicate::str::contains("Entry")),
);
}
#[test]
fn checks_similar_components() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["-A", "--check", "greet"])
.assert()
.success()
.stdout(predicate::str::contains("greet").or(predicate::str::contains("greeting")));
}
#[test]
fn analyzes_impact() {
let fixture = fixtures_path().join("simple_ts");
loctree().current_dir(&fixture).assert().success();
loctree()
.current_dir(&fixture)
.args(["-A", "--impact", "src/utils/greeting.ts"])
.assert()
.success()
.stdout(predicate::str::contains("index.ts"));
}
#[test]
fn finds_symbol() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["-A", "--symbol", "greet"])
.assert()
.success()
.stdout(predicate::str::contains("greeting.ts"));
}
#[test]
fn outputs_sarif() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["-A", "--sarif"])
.assert()
.success()
.stdout(predicate::str::contains(r#""$schema""#))
.stdout(predicate::str::contains("sarif"));
}
#[test]
fn outputs_json() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["-A", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{"));
}
}
mod tauri_mode {
use super::*;
#[test]
fn detects_tauri_project() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.assert()
.success()
.stderr(predicate::str::contains("handlers")); }
#[test]
fn analyzes_tauri_handlers() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args(["-A", "--preset-tauri", "src", "src-tauri/src"])
.assert()
.success();
}
#[test]
fn detects_missing_handlers() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args(["-A", "--preset-tauri", "src", "src-tauri/src"])
.assert()
.success()
.stdout(predicate::str::contains("missing_handler"));
}
#[test]
fn detects_unused_handlers() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args(["-A", "--preset-tauri", "src", "src-tauri/src"])
.assert()
.success()
.stdout(predicate::str::contains("unused_handler"));
}
}
mod ci_fail_flags {
use super::*;
#[test]
fn fails_on_missing_handlers() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args([
"-A",
"--preset-tauri",
"src",
"src-tauri/src",
"--fail-on-missing-handlers",
])
.assert()
.failure()
.code(1);
}
#[test]
fn passes_when_no_missing_handlers() {
let temp = TempDir::new().unwrap();
let fixture = fixtures_path().join("simple_ts");
copy_dir_all(&fixture, temp.path()).unwrap();
loctree()
.current_dir(temp.path())
.args(["-A", "--fail-on-missing-handlers"])
.assert()
.success();
}
}
mod confidence_scoring {
use super::*;
#[test]
fn filters_high_confidence() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["-A", "--dead", "--confidence", "high"])
.assert()
.success();
}
#[test]
fn filters_low_confidence() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["-A", "--dead", "--confidence", "low"])
.assert()
.success();
}
#[test]
fn shows_all_confidence_levels() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["-A", "--dead", "--confidence", "all"])
.assert()
.success();
}
}
mod trace_command {
use super::*;
#[test]
fn traces_handler() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args(["trace", "unused_handler", "src", "src-tauri/src"])
.assert()
.success();
}
}
mod git_commands {
use super::*;
use std::process::Command;
use tempfile::TempDir;
fn create_test_git_repo() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path();
Command::new("git")
.args(["init"])
.current_dir(path)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(path)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(path)
.output()
.unwrap();
std::fs::write(path.join("main.ts"), "export function main() {}").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(path)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.unwrap();
std::fs::write(
path.join("utils.ts"),
"export function add(a: number, b: number) { return a + b; }",
)
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(path)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "Add utils"])
.current_dir(path)
.output()
.unwrap();
temp_dir
}
#[test]
fn git_compare_shows_json_output() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "compare", "HEAD~1", "HEAD"])
.assert()
.success()
.stdout(predicate::str::contains("from_commit"))
.stdout(predicate::str::contains("to_commit"))
.stdout(predicate::str::contains("files"))
.stdout(predicate::str::contains("impact"));
}
#[test]
fn git_compare_with_range_notation() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "compare", "HEAD~1..HEAD"])
.assert()
.success()
.stdout(predicate::str::contains("from_commit"));
}
#[test]
fn git_compare_shows_added_files() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "compare", "HEAD~1", "HEAD"])
.assert()
.success()
.stdout(predicate::str::contains("utils.ts"));
}
#[test]
fn git_command_fails_in_non_git_dir() {
let temp_dir = TempDir::new().unwrap();
std::fs::write(temp_dir.path().join("test.txt"), "hello").unwrap();
loctree()
.current_dir(temp_dir.path())
.args(["git", "compare", "HEAD~1"])
.assert()
.failure()
.stderr(predicate::str::contains("not a git repository"));
}
#[test]
fn git_blame_returns_not_implemented() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "blame", "main.ts"])
.assert()
.success()
.stdout(predicate::str::contains("not_implemented"))
.stdout(predicate::str::contains("Phase 2"));
}
#[test]
fn git_history_returns_not_implemented() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "history", "main"])
.assert()
.success()
.stdout(predicate::str::contains("not_implemented"))
.stdout(predicate::str::contains("Phase 3"));
}
#[test]
fn git_when_introduced_returns_not_implemented() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "when-introduced", "--dead", "unused_fn"])
.assert()
.success()
.stdout(predicate::str::contains("not_implemented"))
.stdout(predicate::str::contains("Phase 3"));
}
#[test]
fn git_compare_shows_commit_info() {
let temp_dir = create_test_git_repo();
loctree()
.current_dir(temp_dir.path())
.args(["git", "compare", "HEAD~1", "HEAD"])
.assert()
.success()
.stdout(predicate::str::contains("Initial commit"))
.stdout(predicate::str::contains("Add utils"));
}
}
mod impact_mode {
use super::*;
fn ensure_snapshot(fixture: &std::path::Path) {
loctree().current_dir(fixture).assert().success();
}
#[test]
fn impact_shows_direct_consumers() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["impact", "src/utils/greeting.ts"])
.assert()
.success()
.stdout(predicate::str::contains("Impact analysis"))
.stdout(predicate::str::contains("Direct consumers"));
}
#[test]
fn impact_shows_transitive_consumers() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["impact", "src/utils/date.ts"])
.assert()
.success()
.stdout(predicate::str::contains("Impact analysis"));
}
#[test]
fn impact_no_consumers_safe_to_remove() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["impact", "src/index.ts"])
.assert()
.success()
.stdout(predicate::str::contains("Impact analysis"));
}
#[test]
fn impact_json_output() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["impact", "src/utils/greeting.ts", "--json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""target""#))
.stdout(predicate::str::contains(r#""direct_consumers""#))
.stdout(predicate::str::contains(r#""transitive_consumers""#))
.stdout(predicate::str::contains(r#""total_affected""#));
}
#[test]
fn impact_with_max_depth() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["impact", "src/utils/greeting.ts", "--max-depth", "1"])
.assert()
.success()
.stdout(predicate::str::contains("Impact analysis"));
}
#[test]
fn impact_file_not_found() {
let fixture = fixtures_path().join("simple_ts");
ensure_snapshot(&fixture);
loctree()
.current_dir(&fixture)
.args(["impact", "nonexistent.ts"])
.assert()
.failure()
.stderr(predicate::str::contains("File not found in snapshot"));
}
#[test]
fn impact_without_snapshot_auto_scans() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/test.ts"), "export const x = 1;").unwrap();
loctree()
.current_dir(temp.path())
.args(["impact", "src/test.ts"])
.assert()
.success()
.stderr(predicate::str::contains("running initial scan"));
}
}
mod diff_mode_new_features {
use super::*;
#[test]
fn diff_help_shows_auto_scan_base_flag() {
loctree()
.args(["diff", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("auto-scan-base")
.or(predicate::str::contains("Auto-scan base commit")),
);
}
#[test]
fn diff_auto_scan_base_flag_exists() {
loctree()
.args(["diff", "--auto-scan-base", "--help"])
.assert()
.success();
}
}
mod watch_mode {
use super::*;
#[test]
fn watch_help_shows_flag() {
loctree().arg("--help").assert().success().stdout(
predicate::str::contains("watch")
.or(predicate::str::contains("Watch for file changes")),
);
}
#[test]
fn watch_flag_recognized() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["scan", "--watch"])
.timeout(std::time::Duration::from_millis(100))
.assert()
.interrupted(); }
}
fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let dest_path = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&entry.path(), &dest_path)?;
} else {
std::fs::copy(entry.path(), dest_path)?;
}
}
Ok(())
}
fn copy_dir_excluding(
src: &std::path::Path,
dst: &std::path::Path,
exclude: &str,
) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
if entry.file_name() == exclude {
continue;
}
let ty = entry.file_type()?;
let dest_path = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&entry.path(), &dest_path)?;
} else {
std::fs::copy(entry.path(), dest_path)?;
}
}
Ok(())
}
fn run_git(repo: &std::path::Path, args: &[&str]) {
let output = std::process::Command::new("git")
.args(args)
.current_dir(repo)
.output()
.unwrap_or_else(|e| panic!("failed to run git {:?}: {e}", args));
assert!(
output.status.success(),
"git {:?} failed.\nstdout: {}\nstderr: {}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
mod instant_commands {
use super::*;
#[test]
fn focus_help_shows_usage() {
loctree()
.args(["focus", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("focus").or(predicate::str::contains("directory")));
}
#[test]
fn focus_on_directory() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["focus", "src/"])
.assert()
.success()
.stdout(
predicate::str::contains("src")
.or(predicate::str::contains("Focus"))
.or(predicate::str::contains("files")),
);
}
#[test]
fn focus_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["focus", "src/", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn hotspots_help_shows_usage() {
loctree()
.args(["hotspots", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("hotspots")
.or(predicate::str::contains("import"))
.or(predicate::str::contains("frequency")),
);
}
#[test]
fn hotspots_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["hotspots"])
.assert()
.success();
}
#[test]
fn hotspots_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["hotspots", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn health_help_shows_usage() {
loctree()
.args(["health", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("health").or(predicate::str::contains("check")));
}
#[test]
fn health_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["health"])
.assert()
.success()
.stdout(
predicate::str::contains("Health")
.or(predicate::str::contains("OK"))
.or(predicate::str::contains("score")),
);
}
#[test]
fn health_alias_h() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["h"])
.assert()
.success();
}
#[test]
fn health_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["health", "--json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""health"#).or(predicate::str::starts_with("{")));
}
#[test]
fn query_help_shows_usage() {
loctree()
.args(["query", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("query")
.or(predicate::str::contains("who-imports"))
.or(predicate::str::contains("where-symbol")),
);
}
#[test]
fn query_who_imports() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["query", "who-imports", "src/utils/greeting.ts"])
.assert()
.success();
}
#[test]
fn query_where_symbol() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["query", "where-symbol", "greet"])
.assert()
.success();
}
#[test]
fn query_alias_q() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["q", "who-imports", "src/utils/greeting.ts"])
.assert()
.success();
}
#[test]
fn commands_help_shows_usage() {
loctree()
.args(["commands", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("commands")
.or(predicate::str::contains("Tauri"))
.or(predicate::str::contains("handler")),
);
}
#[test]
fn commands_in_tauri_project() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args(["commands"])
.assert()
.success();
}
#[test]
fn commands_json_output() {
let fixture = fixtures_path().join("tauri_app");
loctree()
.current_dir(&fixture)
.args(["commands", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn events_help_shows_usage() {
loctree()
.args(["events", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("events")
.or(predicate::str::contains("emit"))
.or(predicate::str::contains("listen")),
);
}
#[test]
fn events_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["events"])
.assert()
.success();
}
#[test]
fn events_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["events", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn coverage_help_shows_usage() {
loctree()
.args(["coverage", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("coverage")
.or(predicate::str::contains("test"))
.or(predicate::str::contains("gaps")),
);
}
#[test]
fn coverage_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["coverage"])
.assert()
.success();
}
#[test]
fn coverage_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["coverage", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
}
mod analysis_commands {
use super::*;
#[test]
fn dead_help_shows_usage() {
loctree()
.args(["dead", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("dead")
.or(predicate::str::contains("unused"))
.or(predicate::str::contains("exports")),
);
}
#[test]
fn dead_detects_unused_exports() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["dead"])
.assert()
.success()
.stdout(
predicate::str::contains("dead")
.or(predicate::str::contains("unused"))
.or(predicate::str::contains("DEAD_CONSTANT")),
);
}
#[test]
fn dead_alias_d() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["d"])
.assert()
.success();
}
#[test]
fn dead_json_output() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["dead", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn cycles_help_shows_usage() {
loctree()
.args(["cycles", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("cycles")
.or(predicate::str::contains("circular"))
.or(predicate::str::contains("imports")),
);
}
#[test]
fn cycles_detects_circular_imports() {
let fixture = fixtures_path().join("circular_imports");
loctree()
.current_dir(&fixture)
.args(["cycles"])
.assert()
.success()
.stdout(
predicate::str::contains("cycle")
.or(predicate::str::contains("circular"))
.or(predicate::str::contains("→")),
);
}
#[test]
fn cycles_alias_c() {
let fixture = fixtures_path().join("circular_imports");
loctree()
.current_dir(&fixture)
.args(["c"])
.assert()
.success();
}
#[test]
fn cycles_json_output() {
let fixture = fixtures_path().join("circular_imports");
loctree()
.current_dir(&fixture)
.args(["cycles", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn twins_help_shows_usage() {
loctree()
.args(["twins", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("twins")
.or(predicate::str::contains("duplicate"))
.or(predicate::str::contains("dead parrot")),
);
}
#[test]
fn twins_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["twins"])
.assert()
.success();
}
#[test]
fn twins_alias_t() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["t"])
.assert()
.success();
}
#[test]
fn twins_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["twins", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn zombie_help_shows_usage() {
loctree()
.args(["zombie", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("zombie")
.or(predicate::str::contains("dead"))
.or(predicate::str::contains("orphan")),
);
}
#[test]
fn zombie_runs_successfully() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["zombie"])
.assert()
.success();
}
#[test]
fn zombie_json_output() {
let fixture = fixtures_path().join("dead_code");
loctree()
.current_dir(&fixture)
.args(["zombie", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn audit_help_shows_usage() {
loctree()
.args(["audit", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("audit")
.or(predicate::str::contains("full"))
.or(predicate::str::contains("codebase")),
);
}
#[test]
fn audit_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["audit", "--no-open"])
.assert()
.success();
}
#[test]
fn audit_stdout_flag() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["audit", "--stdout"])
.assert()
.failure()
.stderr(predicate::str::contains(
"writes markdown reports to an artifact file only",
));
}
#[test]
fn crowd_help_shows_usage() {
loctree()
.args(["crowd", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("crowd")
.or(predicate::str::contains("cluster"))
.or(predicate::str::contains("keyword")),
);
}
#[test]
fn crowd_with_keyword() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["crowd", "greet"])
.assert()
.success();
}
#[test]
fn crowd_json_output() {
let temp = TempDir::new().unwrap();
let fixture = fixtures_path().join("simple_ts");
copy_dir_excluding(&fixture, temp.path(), ".loctree").unwrap();
loctree()
.current_dir(temp.path())
.args(["crowd", "greet", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn tagmap_help_shows_usage() {
loctree()
.args(["tagmap", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("tagmap")
.or(predicate::str::contains("search"))
.or(predicate::str::contains("unified")),
);
}
#[test]
fn tagmap_with_keyword() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["tagmap", "greet"])
.assert()
.success();
}
#[test]
fn tagmap_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["tagmap", "greet", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn plan_help_shows_usage() {
loctree()
.args(["plan", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("plan").or(predicate::str::contains("refactor")));
}
#[test]
fn plan_supports_multiple_targets() {
let fixture = fixtures_path().join("plan_multi");
let temp = TempDir::new().unwrap();
copy_dir_all(&fixture, temp.path()).unwrap();
loctree()
.current_dir(temp.path())
.args(["plan", "--json", "src", "other"])
.assert()
.success()
.stdout(predicate::str::starts_with("["))
.stdout(predicate::str::contains("\"target\": \"src\""))
.stdout(predicate::str::contains("\"target\": \"other\""));
}
#[test]
fn plan_target_layout_affects_move_targets() {
let fixture = fixtures_path().join("plan_multi");
let temp = TempDir::new().unwrap();
copy_dir_all(&fixture, temp.path()).unwrap();
loctree()
.current_dir(temp.path())
.args([
"plan",
"--json",
"src",
"--target-layout",
"ui=custom-ui,app=custom-app",
])
.assert()
.success()
.stdout(predicate::str::contains("src/custom-ui"))
.stdout(predicate::str::contains("src/custom-app"));
}
#[test]
fn sniff_help_shows_usage() {
loctree()
.args(["sniff", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("sniff")
.or(predicate::str::contains("smell"))
.or(predicate::str::contains("aggregate")),
);
}
#[test]
fn sniff_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["sniff"])
.assert()
.success();
}
#[test]
fn sniff_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["sniff", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
}
mod management_commands {
use super::*;
#[test]
fn doctor_help_shows_usage() {
loctree()
.args(["doctor", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("doctor")
.or(predicate::str::contains("diagnostic"))
.or(predicate::str::contains("recommendation")),
);
}
#[test]
fn doctor_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["doctor"])
.assert()
.success();
}
#[test]
fn doctor_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["doctor", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn suppress_help_shows_usage() {
loctree()
.args(["suppress", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("suppress")
.or(predicate::str::contains("false positive"))
.or(predicate::str::contains("ignore")),
);
}
#[test]
fn suppress_list_empty() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("main.ts"), "export const x = 1;").unwrap();
loctree()
.current_dir(temp.path())
.args(["suppress", "--list"])
.assert()
.success();
}
#[test]
fn memex_help_shows_usage() {
loctree()
.args(["memex", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("memex")
.or(predicate::str::contains("memory"))
.or(predicate::str::contains("vector")),
);
}
#[test]
fn auto_help_shows_usage() {
loctree()
.args(["auto", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("auto")
.or(predicate::str::contains("scan"))
.or(predicate::str::contains("artifacts")),
);
}
#[test]
fn auto_creates_loctree_dir() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/main.ts"), "export const x = 1;").unwrap();
loctree()
.current_dir(temp.path())
.args(["auto"])
.assert()
.success();
assert!(snapshot_exists(temp.path()));
}
#[test]
fn auto_json_output() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/main.ts"), "export const x = 1;").unwrap();
loctree()
.current_dir(temp.path())
.args(["auto", "--json"])
.assert()
.success();
assert!(snapshot_exists(temp.path()));
}
#[test]
fn tree_help_shows_usage() {
loctree()
.args(["tree", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("tree")
.or(predicate::str::contains("directory"))
.or(predicate::str::contains("LOC")),
);
}
#[test]
fn tree_shows_structure() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["tree"])
.assert()
.success()
.stdout(
predicate::str::contains("src")
.or(predicate::str::contains("├"))
.or(predicate::str::contains("â””")),
);
}
#[test]
fn tree_with_depth() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["tree", "--depth", "1"])
.assert()
.success();
}
#[test]
fn tree_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["tree", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn find_help_shows_usage() {
loctree()
.args(["find", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("find")
.or(predicate::str::contains("search"))
.or(predicate::str::contains("symbol")),
);
}
#[test]
fn find_symbol() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["find", "greet"])
.assert()
.success()
.stdout(predicate::str::contains("greet").or(predicate::str::contains("greeting")));
}
#[test]
fn find_alias_f() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["f", "greet"])
.assert()
.success();
}
#[test]
fn find_json_output() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["find", "greet", "--json"])
.assert()
.success()
.stdout(predicate::str::starts_with("{").or(predicate::str::starts_with("[")));
}
#[test]
fn report_help_shows_usage() {
loctree()
.args(["report", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("report")
.or(predicate::str::contains("HTML"))
.or(predicate::str::contains("generate")),
);
}
#[test]
fn report_creates_html() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/main.ts"), "export const x = 1;").unwrap();
loctree().current_dir(temp.path()).assert().success();
loctree()
.current_dir(temp.path())
.args(["report"])
.assert()
.success();
}
#[test]
fn lint_help_shows_usage() {
loctree()
.args(["lint", "--help"])
.assert()
.success()
.stdout(
predicate::str::contains("lint")
.or(predicate::str::contains("policy"))
.or(predicate::str::contains("structural")),
);
}
#[test]
fn lint_runs_successfully() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["lint"])
.assert()
.success();
}
#[test]
fn lint_with_fail_flag() {
let fixture = fixtures_path().join("simple_ts");
let _ = loctree()
.current_dir(&fixture)
.args(["lint", "--fail"])
.assert(); }
#[test]
fn lint_sarif_flag_recognized() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["lint", "--sarif"])
.assert()
.stdout(predicate::str::contains("\"version\""))
.stdout(predicate::str::contains("\"runs\""))
.success();
}
}
mod framework_commands {
use super::*;
#[test]
fn routes_help_shows_usage() {
let output = loctree().args(["routes", "--help"]).output().unwrap();
let combined = String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr);
assert!(
combined.contains("routes")
|| combined.contains("FastAPI")
|| combined.contains("Flask"),
"Help should mention routes/FastAPI/Flask: {}",
combined
);
}
#[test]
fn routes_no_routes_in_ts_project() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["routes"])
.assert()
.success()
.stdout(predicate::str::contains("No routes detected"));
}
#[test]
fn routes_json_output_empty() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["routes", "--json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""routes""#))
.stdout(predicate::str::contains(r#""summary""#))
.stdout(predicate::str::contains(r#""count""#));
}
#[test]
fn routes_with_framework_filter() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["routes", "--framework", "fastapi"])
.assert()
.success()
.stdout(predicate::str::contains("No routes detected"));
}
#[test]
fn routes_with_path_filter() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["routes", "--path", "/api/v1"])
.assert()
.success()
.stdout(predicate::str::contains("No routes detected"));
}
#[test]
fn routes_in_python_fixture() {
let temp = TempDir::new().unwrap();
std::fs::write(
temp.path().join("main.py"),
r#"from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "ok"}
@app.post("/users")
def create_user(name: str):
return {"name": name}
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
"#,
)
.unwrap();
loctree()
.current_dir(temp.path())
.args(["routes"])
.assert()
.success()
.stdout(
predicate::str::contains("/health")
.or(predicate::str::contains("route"))
.or(predicate::str::contains("No routes")), );
}
#[test]
fn dist_help_shows_usage() {
let output = loctree().args(["dist", "--help"]).output().unwrap();
let combined = String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr);
assert!(
combined.contains("one or more production source maps")
&& combined.contains("--source-map")
&& combined.contains("--report"),
"Help should explain the dist mental model and report option: {}",
combined
);
}
#[test]
fn dist_requires_source_map() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/index.ts"), "export const x = 1;").unwrap();
loctree()
.current_dir(temp.path())
.args(["dist", "--src", "src/"])
.assert()
.failure()
.stderr(
predicate::str::contains("source-map")
.and(predicate::str::contains("at least one")),
);
}
#[test]
fn dist_requires_src() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("main.js.map"), "{}").unwrap();
loctree()
.current_dir(temp.path())
.args(["dist", "--source-map", "main.js.map"])
.assert()
.failure()
.stderr(predicate::str::contains("src").or(predicate::str::contains("required")));
}
#[test]
fn dist_handles_missing_source_map_file() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/index.ts"), "export const x = 1;").unwrap();
loctree()
.current_dir(temp.path())
.args([
"dist",
"--source-map",
"nonexistent.js.map",
"--src",
"src/",
])
.assert()
.failure()
.stderr(
predicate::str::contains("does not exist")
.or(predicate::str::contains("not found"))
.or(predicate::str::contains("Failed"))
.or(predicate::str::contains("error")),
);
}
#[test]
fn dist_handles_invalid_source_map() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/index.ts"), "export const x = 1;").unwrap();
std::fs::write(temp.path().join("main.js.map"), "not valid json").unwrap();
loctree()
.current_dir(temp.path())
.args(["dist", "--source-map", "main.js.map", "--src", "src/"])
.assert()
.failure()
.stderr(
predicate::str::contains("parse")
.or(predicate::str::contains("invalid"))
.or(predicate::str::contains("Failed"))
.or(predicate::str::contains("error")),
);
}
#[test]
fn dist_falls_back_to_line_level_when_symbol_names_are_unavailable() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(
temp.path().join("src/index.ts"),
"export const hello = 'world';",
)
.unwrap();
let source_map = r#"{
"version": 3,
"file": "main.js",
"sources": ["src/index.ts"],
"sourcesContent": ["export const hello = 'world';"],
"names": ["hello"],
"mappings": "AAAA"
}"#;
std::fs::write(temp.path().join("main.js.map"), source_map).unwrap();
let output = loctree()
.current_dir(temp.path())
.args([
"dist",
"--source-map",
"main.js.map",
"--src",
"src/",
"--json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"dist should succeed with line-level fallback: {}",
String::from_utf8_lossy(&output.stderr)
);
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["analysisLevel"], "line");
assert_eq!(report["symbolLevel"], false);
assert_eq!(
report["deadExports"]
.as_array()
.map(|exports| exports.len()),
Some(0)
);
}
#[test]
fn dist_aggregates_multiple_source_maps() {
let temp = TempDir::new().unwrap();
let fixture = fixtures_path().join("dist_multi_map");
copy_dir_all(&fixture, temp.path()).unwrap();
let output = loctree()
.current_dir(temp.path())
.args([
"dist",
"--src",
"src/",
"--source-map",
"dist/client.js.map",
"--source-map",
"dist/admin.js.map",
"--json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"multi-map dist should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
let dead_exports = report["deadExports"].as_array().unwrap();
let dead_names: Vec<_> = dead_exports
.iter()
.filter_map(|entry| entry.get("name").and_then(|value| value.as_str()))
.collect();
assert_eq!(report["sourceMaps"], 2);
assert_eq!(report["analysisLevel"], "symbol");
assert!(dead_names.contains(&"dead"));
assert!(!dead_names.contains(&"shared"));
assert!(!dead_names.contains(&"clientOnly"));
assert!(!dead_names.contains(&"adminOnly"));
}
#[test]
fn dist_directory_discovery_classifies_runtime_candidates() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::create_dir_all(temp.path().join("dist")).unwrap();
std::fs::write(
temp.path().join("src/index.ts"),
r#"
import { bootOnly } from "./boot";
import { lazyThing } from "./lazy";
export async function loadLazy() {
return import("./lazy");
}
export async function loadFeature() {
return import("./feature");
}
export const app = bootOnly + lazyThing;
"#,
)
.unwrap();
std::fs::write(
temp.path().join("src/boot.ts"),
"export const bootOnly = 1;",
)
.unwrap();
std::fs::write(
temp.path().join("src/lazy.ts"),
"export const lazyThing = 2;",
)
.unwrap();
std::fs::write(
temp.path().join("src/feature.ts"),
"export const featureOnly = 3;",
)
.unwrap();
std::fs::write(
temp.path().join("src/dead.ts"),
"export const deadThing = 4;",
)
.unwrap();
std::fs::write(
temp.path().join("src/verify.ts"),
"export const verifyThing = 5;",
)
.unwrap();
let boot_map = r#"{
"version": 3,
"file": "main.js",
"sources": ["src/index.ts", "src/boot.ts", "src/lazy.ts", "src/verify.ts"],
"names": [],
"mappings": ""
}"#;
let feature_map = r#"{
"version": 3,
"file": "feature.js",
"sources": ["src/feature.ts", "src/verify.ts"],
"names": [],
"mappings": ""
}"#;
std::fs::write(temp.path().join("dist/main.js.map"), boot_map).unwrap();
std::fs::write(temp.path().join("dist/feature.js.map"), feature_map).unwrap();
let output = loctree()
.current_dir(temp.path())
.args(["dist", "--src", "src/", "--source-map", "dist/", "--json"])
.output()
.unwrap();
assert!(
output.status.success(),
"dist directory discovery should succeed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["sourceMaps"], 2);
assert_eq!(report["candidateCounts"]["dead_in_all_chunks"], 1);
assert_eq!(report["candidateCounts"]["boot_path_only"], 1);
assert_eq!(report["candidateCounts"]["feature_local"], 1);
assert_eq!(report["candidateCounts"]["fake_lazy"], 1);
assert_eq!(report["candidateCounts"]["verify_first"], 1);
let candidates = report["candidates"].as_array().unwrap();
assert!(candidates.iter().any(|candidate| {
candidate["class"] == "dead_in_all_chunks" && candidate["name"] == "deadThing"
}));
assert!(candidates.iter().any(|candidate| {
candidate["class"] == "boot_path_only" && candidate["name"] == "bootOnly"
}));
assert!(candidates.iter().any(|candidate| {
candidate["class"] == "feature_local" && candidate["name"] == "featureOnly"
}));
assert!(candidates.iter().any(|candidate| {
candidate["class"] == "fake_lazy" && candidate["name"] == "lazyThing"
}));
assert!(candidates.iter().any(|candidate| {
candidate["class"] == "verify_first" && candidate["name"] == "verifyThing"
}));
}
#[test]
fn dist_writes_json_report() {
let temp = TempDir::new().unwrap();
let fixture = fixtures_path().join("dist_multi_map");
copy_dir_all(&fixture, temp.path()).unwrap();
let report_path = temp.path().join(".loctree/dist-report.json");
loctree()
.current_dir(temp.path())
.args([
"dist",
"--src",
"src/",
"--source-map",
"dist/client.js.map",
"--source-map",
"dist/admin.js.map",
"--report",
report_path.to_str().unwrap(),
])
.assert()
.success()
.stdout(
predicate::str::contains("Report:")
.and(predicate::str::contains(".loctree/dist-report.json")),
);
let report_contents = std::fs::read_to_string(&report_path).unwrap();
let report: Value = serde_json::from_str(&report_contents).unwrap();
assert_eq!(report["sourceMaps"], 2);
assert_eq!(report["analysisLevel"], "symbol");
assert!(
report["deadExports"]
.as_array()
.unwrap()
.iter()
.any(|entry| entry["name"] == "dead")
);
}
#[test]
fn dist_uses_explicit_src_scope_instead_of_repo_root_snapshot() {
let temp = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(
temp.path().join("src/index.ts"),
"export const live = 'world';",
)
.unwrap();
std::fs::write(temp.path().join("other.ts"), "export const repoOnly = 1;").unwrap();
let source_map = r#"{
"version": 3,
"file": "main.js",
"sources": ["src/index.ts"],
"sourcesContent": ["export const live = 'world';"],
"names": [],
"mappings": "AAAA"
}"#;
std::fs::write(temp.path().join("main.js.map"), source_map).unwrap();
loctree()
.current_dir(temp.path())
.env("LOCT_CACHE_DIR", cache.path())
.assert()
.success();
let output = loctree()
.current_dir(temp.path())
.env("LOCT_CACHE_DIR", cache.path())
.args([
"dist",
"--source-map",
"main.js.map",
"--src",
"src/",
"--json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"dist failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["sourceExports"], 1);
assert_eq!(report["bundledExports"], 1);
assert_eq!(
report["deadExports"]
.as_array()
.map(|exports| exports.len()),
Some(0)
);
}
#[test]
fn dist_refreshes_stale_strict_src_snapshot() {
let temp = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("src")).unwrap();
std::fs::write(temp.path().join("src/index.ts"), "export const live = 1;").unwrap();
let source_map = r#"{
"version": 3,
"file": "main.js",
"sources": ["src/index.ts"],
"sourcesContent": ["export const live = 1;"],
"names": [],
"mappings": "AAAA"
}"#;
std::fs::write(temp.path().join("main.js.map"), source_map).unwrap();
run_git(temp.path(), &["init"]);
run_git(temp.path(), &["config", "user.email", "test@example.com"]);
run_git(temp.path(), &["config", "user.name", "Test User"]);
run_git(temp.path(), &["add", "."]);
run_git(temp.path(), &["commit", "-m", "init"]);
let first = loctree()
.current_dir(temp.path())
.env("LOCT_CACHE_DIR", cache.path())
.args([
"dist",
"--source-map",
"main.js.map",
"--src",
"src/",
"--json",
])
.output()
.unwrap();
assert!(
first.status.success(),
"initial dist failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&first.stdout),
String::from_utf8_lossy(&first.stderr)
);
std::fs::write(temp.path().join("src/new.ts"), "export const stray = 1;").unwrap();
let second = loctree()
.current_dir(temp.path())
.env("LOCT_CACHE_DIR", cache.path())
.args([
"dist",
"--source-map",
"main.js.map",
"--src",
"src/",
"--json",
])
.output()
.unwrap();
assert!(
second.status.success(),
"second dist failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&second.stdout),
String::from_utf8_lossy(&second.stderr)
);
let report: Value = serde_json::from_slice(&second.stdout).unwrap();
assert_eq!(report["sourceExports"], 2);
assert_eq!(report["bundledExports"], 1);
let dead_exports = report["deadExports"]
.as_array()
.cloned()
.unwrap_or_default();
assert_eq!(dead_exports.len(), 1);
assert_eq!(dead_exports[0]["file"].as_str(), Some("new.ts"));
}
#[test]
fn layoutmap_help_shows_usage() {
loctree()
.args(["layoutmap", "--help"])
.assert()
.success() .stdout(
predicate::str::contains("layoutmap")
.or(predicate::str::contains("z-index"))
.or(predicate::str::contains("CSS")),
);
}
#[test]
fn layoutmap_no_css_in_ts_project() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["layoutmap"])
.assert()
.success()
.stdout(
predicate::str::contains("No CSS")
.or(predicate::str::contains("findings"))
.or(predicate::str::contains("0")),
);
}
#[test]
fn layoutmap_json_output_empty() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["layoutmap", "--json"])
.assert()
.success()
.stdout(predicate::str::contains("[").or(predicate::str::contains("{")));
}
#[test]
fn layoutmap_with_zindex_filter() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["layoutmap", "--zindex"])
.assert()
.success();
}
#[test]
fn layoutmap_with_sticky_filter() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["layoutmap", "--sticky"])
.assert()
.success();
}
#[test]
fn layoutmap_with_grid_filter() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["layoutmap", "--grid"])
.assert()
.success();
}
#[test]
fn layoutmap_with_min_zindex() {
let fixture = fixtures_path().join("simple_ts");
loctree()
.current_dir(&fixture)
.args(["layoutmap", "--min-zindex", "100"])
.assert()
.success();
}
#[test]
fn layoutmap_with_css_content() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("styles")).unwrap();
std::fs::write(
temp.path().join("styles/main.css"),
r#"
.modal {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
}
.tooltip {
position: absolute;
z-index: 500;
}
.header {
position: sticky;
top: 0;
z-index: 100;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
}
.flex-row {
display: flex;
flex-direction: row;
}
"#,
)
.unwrap();
loctree()
.current_dir(temp.path())
.args(["layoutmap"])
.assert()
.success()
.stdout(
predicate::str::contains("z-index")
.or(predicate::str::contains("1000"))
.or(predicate::str::contains("modal"))
.or(predicate::str::contains("LAYERS"))
.or(predicate::str::contains("findings")),
);
}
#[test]
fn layoutmap_json_with_css() {
let temp = TempDir::new().unwrap();
std::fs::write(
temp.path().join("app.css"),
r#"
.overlay {
position: fixed;
z-index: 9999;
}
"#,
)
.unwrap();
loctree()
.current_dir(temp.path())
.args(["layoutmap", "--json"])
.assert()
.success()
.stdout(predicate::str::contains("[").or(predicate::str::contains("{")));
}
#[test]
fn layoutmap_exclude_pattern() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("node_modules")).unwrap();
std::fs::write(
temp.path().join("node_modules/lib.css"),
".x { z-index: 1000; }",
)
.unwrap();
std::fs::write(temp.path().join("main.css"), ".y { z-index: 100; }").unwrap();
loctree()
.current_dir(temp.path())
.args(["layoutmap", "--exclude", "**/node_modules/**"])
.assert()
.success();
}
}