use std::process::Command;
use std::io::Write;
fn cx() -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_cx"));
cmd.current_dir(env!("CARGO_MANIFEST_DIR"));
cmd
}
fn temp_project(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
for (path, content) in files {
let full = dir.path().join(path);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let mut f = std::fs::File::create(&full).unwrap();
f.write_all(content.as_bytes()).unwrap();
}
dir
}
fn cx_in(dir: &std::path::Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_cx"));
cmd.current_dir(dir);
cmd
}
fn two_projects() -> (tempfile::TempDir, tempfile::TempDir) {
let a = temp_project(&[
("src/alpha.rs", "pub fn alpha_fn() {}\npub struct AlphaType;\n"),
("src/shared.rs", "pub fn shared() { 1 }\n"),
]);
let b = temp_project(&[
("src/beta.ts", "export function betaFn() {}\nexport class BetaClass {}\n"),
("src/shared.rs", "pub fn shared() { 2 }\n"),
]);
(a, b)
}
#[test]
fn overview_main_rs() {
let out = cx().args(["overview", "src/main.rs"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("{name,kind,range,signature}:"), "should have TOON header: {stdout}");
assert!(stdout.contains("main,fn,"));
assert!(stdout.contains("resolve_root,fn,"));
}
#[test]
fn overview_includes_line_ranges() {
let dir = temp_project(&[(
"src/lib.rs",
"pub fn alpha() {\n beta();\n}\n\npub fn beta() {}\n",
)]);
let out = cx_in(dir.path())
.args(["overview", "src/lib.rs"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("{name,kind,range,signature}:"), "{stdout}");
assert!(stdout.contains("alpha,fn,\"1-3\","), "{stdout}");
assert!(stdout.contains("beta,fn,\"5\","), "{stdout}");
}
#[test]
fn symbols_kind_fn() {
let out = cx().args(["symbols", "--kind", "fn", "--all"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success());
assert!(stdout.contains("main,fn,"));
assert!(stdout.contains("print_toon,fn,"));
}
#[test]
fn definition_main() {
let out = cx().args(["definition", "--name", "main"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success());
assert!(stdout.contains("src/main.rs"), "{stdout}");
assert!(stdout.contains("fn main()"), "{stdout}");
assert!(stdout.contains("Cli::parse()"), "{stdout}");
}
#[test]
fn overview_nonexistent_exits_1() {
let out = cx().args(["overview", "nonexistent.rs"]).output().unwrap();
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("nonexistent.rs"), "stderr should mention the file: {stderr}");
}
#[test]
fn symbols_no_match_exits_0() {
let out = cx().args(["symbols", "--name", "zzz_no_match"]).output().unwrap();
assert_eq!(out.status.code(), Some(0));
}
#[test]
fn json_overview() {
let out = cx().args(["--json", "overview", "src/main.rs"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success());
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.expect("should be valid JSON");
assert!(parsed.is_array());
}
#[test]
fn json_definition_always_array() {
let out = cx().args(["--json", "definition", "--name", "main"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success());
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.expect("should be valid JSON");
assert!(parsed.is_array(), "definition JSON should always be an array: {stdout}");
assert_eq!(parsed.as_array().unwrap().len(), 1);
}
#[test]
fn definition_from_disambiguates() {
let dir = temp_project(&[
("src/a.rs", "pub fn helper() { 1 }\n"),
("src/b.rs", "pub fn helper() { 2 }\n"),
]);
let out = cx_in(dir.path()).args(["--json", "definition", "--name", "helper"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(parsed.as_array().unwrap().len(), 2, "should find both: {stdout}");
let out2 = cx_in(dir.path())
.args(["--json", "definition", "--name", "helper", "--from", "src/a.rs"])
.output()
.unwrap();
let stdout2 = String::from_utf8_lossy(&out2.stdout);
let parsed2: serde_json::Value = serde_json::from_str(&stdout2).unwrap();
let arr = parsed2.as_array().unwrap();
assert_eq!(arr.len(), 1, "should find one: {stdout2}");
assert_eq!(arr[0]["file"].as_str().unwrap(), "src/a.rs");
}
#[test]
fn definition_max_lines_truncates() {
let mut body = String::from("pub fn big() {\n");
for i in 0..250 {
body.push_str(&format!(" let x{i} = {i};\n"));
}
body.push_str("}\n");
let dir = temp_project(&[("src/big.rs", &body)]);
let out = cx_in(dir.path())
.args(["--json", "definition", "--name", "big"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let item = &parsed.as_array().unwrap()[0];
assert_eq!(item["truncated"].as_bool(), Some(true), "should be truncated: {stdout}");
assert!(item["lines"].as_u64().unwrap() > 200, "should report total lines: {stdout}");
}
#[test]
fn cache_path_prints_path() {
let dir = temp_project(&[("src/main.rs", "fn main() {}\n")]);
let out = cx_in(dir.path()).args(["cache", "path"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("indexes/") || stdout.contains("indexes\\"), "should contain indexes path: {stdout}");
assert!(stdout.trim().ends_with(".db"), "should end with .db: {stdout}");
}
#[test]
fn cache_clean_removes_index() {
let dir = temp_project(&[("src/main.rs", "fn main() {}\n")]);
let out = cx_in(dir.path()).args(["overview", "src/main.rs"]).output().unwrap();
assert!(out.status.success());
let out = cx_in(dir.path()).args(["cache", "path"]).output().unwrap();
let cache_path = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert!(std::path::Path::new(&cache_path).exists(), "index should exist after build");
let out = cx_in(dir.path()).args(["cache", "clean"]).output().unwrap();
assert!(out.status.success());
assert!(!std::path::Path::new(&cache_path).exists(), "index should be gone after clean");
}
#[test]
fn index_not_in_repo_root() {
let dir = temp_project(&[("src/main.rs", "fn main() {}\n")]);
let _ = cx_in(dir.path()).args(["overview", "src/main.rs"]).output().unwrap();
assert!(!dir.path().join(".cx-index.db").exists(), "should not create .cx-index.db in repo root");
}
#[test]
fn version_flag() {
let out = cx().arg("--version").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.starts_with("cx "), "should print version: {stdout}");
}
#[test]
fn markdown_overview_shows_headings() {
let dir = temp_project(&[
("README.md", "# Hello\nintro\n\n## Usage\nbody\n"),
("src/main.rs", "fn main() {}\n"),
]);
let out = cx_in(dir.path()).args(["overview", "README.md"]).output().unwrap();
assert!(out.status.success(), "overview README.md should succeed: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("{name,kind,range,signature}:"), "{stdout}");
assert!(stdout.contains("Hello,heading"), "{stdout}");
assert!(stdout.contains("Usage,heading"), "{stdout}");
}
#[test]
fn markdown_definition_returns_section() {
let dir = temp_project(&[
("README.md", "# Hello\nintro\n\n## Usage\nbody\n\n### Details\nmore\n\n## Install\nsteps\n"),
("src/main.rs", "fn main() {}\n"),
]);
let out = cx_in(dir.path())
.args(["definition", "--name", "Usage", "--from", "README.md"])
.output()
.unwrap();
assert!(out.status.success(), "definition Usage should succeed: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("## Usage\nbody"), "{stdout}");
assert!(stdout.contains("### Details\nmore"), "{stdout}");
assert!(!stdout.contains("## Install\nsteps"), "{stdout}");
}
#[test]
fn no_matches_stderr() {
let out = cx().args(["definition", "--name", "zzz_nonexistent"]).output().unwrap();
assert_eq!(out.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("no matches"), "should print no matches: {stderr}");
}
#[test]
fn definition_kind_filter() {
let dir = temp_project(&[
("src/lib.rs", "pub struct Foo;\npub fn Foo() {}\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "definition", "--name", "Foo", "--kind", "fn"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1, "should find only the fn, not the struct: {stdout}");
assert!(arr[0]["body"].as_str().unwrap().contains("fn Foo()"));
}
#[test]
fn references_file_filter() {
let dir = temp_project(&[
("src/a.rs", "pub struct Foo;\nfn use_foo(f: Foo) {}\n"),
("src/b.rs", "use crate::Foo;\nfn bar(f: Foo) {}\n"),
]);
let out = cx_in(dir.path())
.args(["references", "--name", "Foo", "--file", "src/a.rs", "--context"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("src/a.rs"), "should find refs in a.rs: {stdout}");
assert!(!stdout.contains("src/b.rs"), "should not include b.rs: {stdout}");
}
#[test]
fn references_dedup_same_line() {
let dir = temp_project(&[
("src/lib.rs", "fn convert(x: Foo) -> Foo { x }\npub struct Foo;\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "references", "--name", "Foo", "--context"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
let line1_refs: Vec<_> = arr.iter().filter(|r| r["line"] == 1).collect();
assert_eq!(line1_refs.len(), 1, "same-line refs should be deduped: {stdout}");
}
#[test]
fn references_summary_groups_by_file() {
let dir = temp_project(&[
("src/a.rs", "pub struct Foo;\nfn use1(f: Foo) {}\nfn use2(f: Foo) {}\n"),
("src/b.rs", "use crate::Foo;\nfn use3(f: Foo) {}\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "references", "--name", "Foo"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
let a = arr
.iter()
.find(|row| row["file"] == "src/a.rs")
.expect("summary should include src/a.rs");
assert_eq!(a["refs"].as_u64().unwrap(), 3, "{stdout}");
assert_eq!(a["callers"].as_str().unwrap(), "Foo, use1, use2", "{stdout}");
assert_eq!(a["lines"].as_str().unwrap(), "1, 2, 3", "{stdout}");
assert!(arr.iter().any(|row| row["file"] == "src/b.rs"), "{stdout}");
}
#[test]
fn overview_directory_single_level() {
let dir = temp_project(&[
("src/main.rs", "fn main() {}\n"),
("src/lib.rs", "pub fn hello() {}\npub struct Config;\n"),
("src/util/helpers.rs", "pub fn help() {}\n"),
("src/util/math.rs", "pub fn add() {}\npub fn sub() {}\n"),
]);
let out = cx_in(dir.path()).args(["overview", "src/"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("util/"), "should group util as subdir: {stdout}");
assert!(!stdout.contains("helpers.rs"), "should not show nested files: {stdout}");
assert!(stdout.contains("main.rs"), "should show direct file: {stdout}");
assert!(stdout.contains("lib.rs"), "should show direct file: {stdout}");
}
#[test]
fn overview_directory_full_keeps_table_shape_with_ranges() {
let dir = temp_project(&[
("src/main.rs", "fn main() {}\n"),
("src/lib.rs", "pub fn hello() {}\npub struct Config;\n"),
("src/util/helpers.rs", "pub fn help() {}\n"),
]);
let out = cx_in(dir.path())
.args(["overview", "src/", "--full"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("{file,name,kind,range,signature}:"), "{stdout}");
assert!(stdout.contains("src/util/"), "{stdout}");
assert!(stdout.contains("src/lib.rs,hello,fn,\"1\""), "{stdout}");
}
#[test]
fn overview_directory_root() {
let dir = temp_project(&[
("src/main.rs", "fn main() {}\n"),
("src/lib.rs", "pub fn hello() {}\n"),
]);
let out = cx_in(dir.path()).args(["overview", "."]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("src/"), "should show src as subdir: {stdout}");
}
#[test]
fn overview_directory_includes_tests_by_default() {
let dir = temp_project(&[
("src/app.ts", "export function main() {}\n"),
("src/app.test.ts", "export function testHelper() {}\n"),
("tests/integration.rs", "fn test_it() {}\n"),
]);
let out = cx_in(dir.path()).args(["overview", "."]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("tests/"), "should include test dirs: {stdout}");
let out = cx_in(dir.path()).args(["overview", "src/"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("app.test.ts"), "should include test files: {stdout}");
let out = cx_in(dir.path()).args(["overview", ".", "--no-tests"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(!stdout.contains("tests/"), "--no-tests should filter test dirs: {stdout}");
let out = cx_in(dir.path()).args(["overview", "src/", "--no-tests"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(!stdout.contains("app.test.ts"), "--no-tests should filter test files: {stdout}");
}
#[test]
fn overview_directory_nonexistent() {
let dir = temp_project(&[("src/main.rs", "fn main() {}\n")]);
let out = cx_in(dir.path()).args(["overview", "nonexistent/"]).output().unwrap();
assert_eq!(out.status.code(), Some(1));
}
#[test]
fn json_definition_has_expected_fields() {
let out = cx().args(["--json", "definition", "--name", "main"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let item = &parsed.as_array().unwrap()[0];
assert!(item["file"].is_string());
assert!(item["line"].is_number());
assert!(item["body"].is_string());
}
#[test]
fn definition_default_limit_truncates() {
let dir = temp_project(&[
("src/a.rs", "pub fn helper() { 1 }\n"),
("src/b.rs", "pub fn helper() { 2 }\n"),
("src/c.rs", "pub fn helper() { 3 }\n"),
("src/d.rs", "pub fn helper() { 4 }\n"),
("src/e.rs", "pub fn helper() { 5 }\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "definition", "--name", "helper"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_object(), "paginated output should be an object: {stdout}");
assert_eq!(parsed["total"].as_u64().unwrap(), 5);
assert_eq!(parsed["results"].as_array().unwrap().len(), 3);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("3/5"), "should show 3/5 in hint: {stderr}");
assert!(stderr.contains("--offset 3"), "should suggest next offset: {stderr}");
assert!(stderr.contains("--all"), "should suggest --all: {stderr}");
}
#[test]
fn definition_offset_paginates() {
let dir = temp_project(&[
("src/a.rs", "pub fn helper() { 1 }\n"),
("src/b.rs", "pub fn helper() { 2 }\n"),
("src/c.rs", "pub fn helper() { 3 }\n"),
("src/d.rs", "pub fn helper() { 4 }\n"),
("src/e.rs", "pub fn helper() { 5 }\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "definition", "--name", "helper", "--offset", "3"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_object(), "offset > 0 should produce paginated envelope: {stdout}");
assert_eq!(parsed["total"].as_u64().unwrap(), 5);
assert_eq!(parsed["offset"].as_u64().unwrap(), 3);
assert_eq!(parsed["results"].as_array().unwrap().len(), 2);
}
#[test]
fn definition_all_bypasses_limit() {
let dir = temp_project(&[
("src/a.rs", "pub fn helper() { 1 }\n"),
("src/b.rs", "pub fn helper() { 2 }\n"),
("src/c.rs", "pub fn helper() { 3 }\n"),
("src/d.rs", "pub fn helper() { 4 }\n"),
("src/e.rs", "pub fn helper() { 5 }\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "--all", "definition", "--name", "helper"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_array(), "should be bare array with --all: {stdout}");
assert_eq!(parsed.as_array().unwrap().len(), 5);
}
#[test]
fn definition_from_skips_default_limit() {
let dir = temp_project(&[
("src/a.rs", "pub fn thing() { 1 }\npub fn thing2() { 2 }\nstruct thing3;\nstruct thing4;\nenum thing5 {}\n"),
("src/b.rs", "pub fn thing() { 10 }\n"),
("src/c.rs", "pub fn thing() { 20 }\n"),
("src/d.rs", "pub fn thing() { 30 }\n"),
("src/e.rs", "pub fn thing() { 40 }\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "definition", "--name", "thing", "--from", "src/a.rs"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_array(), "should be bare array when --from used: {stdout}");
let arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert!(arr[0]["file"].as_str().unwrap().contains("a.rs"));
let out2 = cx_in(dir.path())
.args(["--json", "definition", "--name", "thing"])
.output()
.unwrap();
let stdout2 = String::from_utf8_lossy(&out2.stdout);
let parsed2: serde_json::Value = serde_json::from_str(&stdout2).unwrap();
assert!(parsed2.is_object(), "without --from should be paginated: {stdout2}");
assert_eq!(parsed2["total"].as_u64().unwrap(), 5);
assert_eq!(parsed2["results"].as_array().unwrap().len(), 3);
}
#[test]
fn definition_explicit_limit_overrides_default() {
let dir = temp_project(&[
("src/a.rs", "pub fn helper() { 1 }\n"),
("src/b.rs", "pub fn helper() { 2 }\n"),
("src/c.rs", "pub fn helper() { 3 }\n"),
("src/d.rs", "pub fn helper() { 4 }\n"),
("src/e.rs", "pub fn helper() { 5 }\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "--limit", "2", "definition", "--name", "helper"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_object(), "should be paginated: {stdout}");
assert_eq!(parsed["results"].as_array().unwrap().len(), 2);
assert_eq!(parsed["total"].as_u64().unwrap(), 5);
}
#[test]
fn symbols_pagination_hint_on_stderr() {
let dir = temp_project(&[
("src/a.rs", "pub fn a1() {}\npub fn a2() {}\n"),
("src/b.rs", "pub fn b1() {}\npub fn b2() {}\n"),
]);
let out = cx_in(dir.path())
.args(["--limit", "2", "symbols"])
.output()
.unwrap();
assert!(out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("2/4"), "should show 2/4: {stderr}");
assert!(stderr.contains("--offset 2"), "should suggest next offset: {stderr}");
}
#[test]
fn references_pagination() {
let dir = temp_project(&[
("src/a.rs", "pub struct Foo;\nfn use1(f: Foo) {}\nfn use2(f: Foo) {}\n"),
("src/b.rs", "use crate::Foo;\nfn use3(f: Foo) {}\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "--limit", "2", "references", "--name", "Foo", "--context"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
if parsed.is_object() {
assert_eq!(parsed["results"].as_array().unwrap().len(), 2);
assert!(parsed["total"].as_u64().unwrap() >= 2);
}
}
#[test]
fn json_paginated_has_metadata() {
let dir = temp_project(&[
("src/a.rs", "pub fn helper() { 1 }\n"),
("src/b.rs", "pub fn helper() { 2 }\n"),
("src/c.rs", "pub fn helper() { 3 }\n"),
("src/d.rs", "pub fn helper() { 4 }\n"),
]);
let out = cx_in(dir.path())
.args(["--json", "--limit", "2", "definition", "--name", "helper"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed["total"].is_number(), "should have total: {stdout}");
assert!(parsed["offset"].is_number(), "should have offset: {stdout}");
assert!(parsed["limit"].is_number(), "should have limit: {stdout}");
assert!(parsed["results"].is_array(), "should have results: {stdout}");
assert_eq!(parsed["offset"].as_u64().unwrap(), 0);
assert_eq!(parsed["limit"].as_u64().unwrap(), 2);
}
#[test]
fn no_pagination_when_under_limit() {
let out = cx().args(["--json", "definition", "--name", "main"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_array(), "single result should be bare array: {stdout}");
}
fn dir_project() -> tempfile::TempDir {
temp_project(&[
("src/lib.rs", "pub fn alpha() {}\npub fn beta() {}\n"),
("src/util.rs", "pub fn gamma() {}\n"),
("tests/test_main.rs", "fn test_alpha() {}\n"),
])
}
#[test]
fn symbols_file_directory_returns_all_files_in_dir() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["symbols", "--kind", "fn", "--file", "src"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("alpha"), "should find alpha in src/: {stdout}");
assert!(stdout.contains("gamma"), "should find gamma in src/: {stdout}");
assert!(!stdout.contains("test_alpha"), "should not include tests/: {stdout}");
}
#[test]
fn symbols_file_directory_shows_file_column() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["--json", "symbols", "--kind", "fn", "--file", "src"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
assert!(arr.iter().all(|r| r.get("file").is_some()), "directory query should include file column: {stdout}");
}
#[test]
fn symbols_file_single_file_hides_file_column() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["--json", "symbols", "--kind", "fn", "--file", "src/lib.rs"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
assert!(arr.iter().all(|r| r.get("file").is_none()), "single-file query should omit file column: {stdout}");
}
#[test]
fn cpp_declaration_only_header_at_nested_path() {
let dir = temp_project(&[(
"GuideModule/algorithm/Guide2DLineDemoAlgorithm.h",
"\u{feff}#pragma once\nclass Guide2DLineDemoAlgorithm {\npublic:\n QString AlgorithmId() const;\n bool Validate(const GuideAlgorithmDataView& ctx, QString& errorMsg) const;\n};\n",
)]);
let out = cx_in(dir.path())
.args(["overview", "GuideModule/algorithm/Guide2DLineDemoAlgorithm.h"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("Guide2DLineDemoAlgorithm,class"), "should find class: {stdout}");
assert!(stdout.contains("AlgorithmId,fn"), "should find declared method: {stdout}");
assert!(stdout.contains("Validate,fn"), "should find declared method: {stdout}");
let out = cx_in(dir.path())
.args(["symbols", "--file", "GuideModule/algorithm/Guide2DLineDemoAlgorithm.h"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("Guide2DLineDemoAlgorithm,class"), "should find class: {stdout}");
assert!(stdout.contains("AlgorithmId,fn"), "should find declared method: {stdout}");
assert!(stdout.contains("Validate,fn"), "should find declared method: {stdout}");
}
#[test]
fn references_file_directory() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["references", "--name", "alpha", "--file", "src"]).output().unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn kind_counts_file_directory() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["symbols", "--kinds", "--file", "src"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("fn"), "should list fn kind: {stdout}");
}
#[test]
fn definition_from_directory() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["definition", "--name", "alpha", "--from", "src"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("alpha"), "should find alpha from src/: {stdout}");
}
#[test]
fn symbols_file_nonexistent_directory() {
let dir = dir_project();
let out = cx_in(dir.path()).args(["symbols", "--file", "nonexistent"]).output().unwrap();
assert_eq!(out.status.code(), Some(1));
}
#[test]
fn symbols_file_empty_directory() {
let dir = dir_project();
std::fs::create_dir(dir.path().join("empty")).unwrap();
let out = cx_in(dir.path()).args(["symbols", "--file", "empty"]).output().unwrap();
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("no indexed files under"), "should report empty dir: {stderr}");
}
#[test]
fn overview_directory_from_subdirectory() {
let dir = dir_project();
let out = cx_in(&dir.path().join("src")).args(["overview", "."]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "overview from subdir should work: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("alpha") || stdout.contains("lib.rs"), "should find files in src/: {stdout}");
}
#[test]
fn overview_absolute_path_resolves_foreign_project() {
let (a, b) = two_projects();
let b_src = b.path().join("src");
let out = cx_in(a.path())
.args(["overview", b_src.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "should succeed: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("beta.ts"), "should find B's files: {stdout}");
assert!(!stdout.contains("alpha.rs"), "should not find A's files: {stdout}");
}
#[test]
fn overview_absolute_file_resolves_foreign_project() {
let (a, b) = two_projects();
let b_file = b.path().join("src/shared.rs");
let out = cx_in(a.path())
.args(["overview", b_file.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "should succeed: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("shared"), "should find shared fn: {stdout}");
}
#[test]
fn symbols_absolute_file_resolves_foreign_project() {
let (a, b) = two_projects();
let b_src = b.path().join("src");
let out = cx_in(a.path())
.args(["symbols", "--file", b_src.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "should succeed: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("betaFn") || stdout.contains("BetaClass"),
"should find B's symbols: {stdout}");
}
#[test]
fn definition_absolute_from_resolves_foreign_project() {
let (a, b) = two_projects();
let b_file = b.path().join("src/shared.rs");
let out = cx_in(a.path())
.args(["--json", "definition", "--name", "shared", "--from", b_file.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "should succeed: {}", String::from_utf8_lossy(&out.stderr));
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert!(arr[0]["body"].as_str().unwrap().contains("2"),
"should get B's definition, not A's: {stdout}");
}
#[test]
fn references_absolute_file_resolves_foreign_project() {
let (a, b) = two_projects();
let b_file = b.path().join("src/shared.rs");
let out = cx_in(a.path())
.args(["references", "--name", "shared", "--file", b_file.to_str().unwrap()])
.output()
.unwrap();
assert!(out.status.success(), "should succeed: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn relative_path_still_uses_cwd_project() {
let (a, _b) = two_projects();
let out = cx_in(a.path())
.args(["overview", "src/"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "relative paths should still work: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.contains("alpha.rs"), "should find A's files: {stdout}");
}
#[test]
fn no_path_arg_uses_cwd_project() {
let (a, _b) = two_projects();
let out = cx_in(a.path())
.args(["symbols", "--name", "alpha_fn"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success());
assert!(stdout.contains("alpha_fn"), "should find A's symbols: {stdout}");
}
#[test]
fn explicit_root_overrides_path_hint() {
let (a, b) = two_projects();
let b_src = b.path().join("src");
let out = cx_in(a.path())
.args(["--root", a.path().to_str().unwrap(), "overview", b_src.to_str().unwrap()])
.output()
.unwrap();
assert!(!out.status.success(), "--root should override path-based discovery");
}