use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::fs;
use tempfile::TempDir;
fn cqs() -> Command {
#[allow(deprecated)]
Command::cargo_bin("cqs").expect("Failed to find cqs binary")
}
fn setup_project() -> TempDir {
let dir = TempDir::new().expect("Failed to create temp dir");
let src_dir = dir.path().join("src");
fs::create_dir(&src_dir).expect("Failed to create src dir");
fs::write(
src_dir.join("lib.rs"),
r#"
/// Adds two numbers
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Subtracts b from a
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
"#,
)
.expect("Failed to write test file");
dir
}
#[test]
fn test_help_output() {
cqs()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Semantic code search"));
}
#[test]
fn test_version_output() {
cqs()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("cqs"));
}
#[test]
#[serial]
fn test_init_creates_cqs_directory() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Created .cqs/"));
assert!(
dir.path().join(".cqs").exists(),
".cqs directory should exist"
);
}
#[test]
#[serial]
fn test_init_idempotent() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
}
#[test]
fn test_stats_requires_init() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["stats"])
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
#[serial]
fn test_stats_shows_counts() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["stats"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Total chunks:"));
}
#[test]
#[serial]
fn test_index_auto_initializes() {
let dir = setup_project();
assert!(
!dir.path().join(".cqs").exists(),
".cqs should not exist before index"
);
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Index complete"));
assert!(
dir.path().join(".cqs").exists(),
".cqs should exist after index"
);
}
#[test]
#[serial]
fn test_index_parses_files() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Index complete"));
}
#[test]
#[serial]
fn test_search_returns_results() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["add numbers"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("results"));
}
#[test]
#[serial]
fn test_search_json_output() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["--json", "add numbers"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("\"name\""));
}
#[test]
fn test_completions_generates_script() {
cqs()
.args(["completions", "bash"])
.assert()
.success()
.stdout(predicate::str::contains("complete"));
}
#[test]
fn test_invalid_option_fails() {
cqs()
.args(["--invalid-option-xyz"])
.assert()
.failure()
.stderr(predicate::str::contains("unexpected argument"));
}
#[test]
#[serial]
fn test_doctor_runs() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["doctor"])
.current_dir(dir.path())
.assert()
.success();
}
#[test]
#[serial]
fn test_doctor_shows_runtime() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["doctor"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Runtime"));
}
#[test]
#[serial]
fn test_doctor_shows_parser() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["doctor"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Parser"));
}
#[test]
fn test_callers_no_index() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["callers", "some_function"])
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("not found").or(predicate::str::contains("Index")));
}
#[test]
fn test_callees_no_index() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["callees", "some_function"])
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("not found").or(predicate::str::contains("Index")));
}
#[test]
#[serial]
fn test_callers_json_output() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
let output = cqs()
.args(["callers", "add", "--json"])
.current_dir(dir.path())
.assert()
.success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("Invalid JSON output: {} — raw: {}", e, stdout));
assert!(parsed.is_array(), "callers --json should return array");
}
#[test]
#[serial]
fn test_callees_json_output() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
let output = cqs()
.args(["callees", "add", "--json"])
.current_dir(dir.path())
.assert()
.success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("Invalid JSON output: {} — raw: {}", e, stdout));
assert!(parsed.is_object(), "callees --json should return object");
assert!(parsed["name"].is_string(), "Should have name field");
}
#[test]
fn test_gc_requires_index() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["gc"])
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("not found").or(predicate::str::contains("Index")));
}
#[test]
#[serial]
fn test_gc_json_on_clean_index() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
let output = cqs()
.args(["gc", "--json"])
.current_dir(dir.path())
.assert()
.success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("Invalid JSON output: {} — raw: {}", e, stdout));
assert_eq!(
parsed["pruned_chunks"], 0,
"Fresh index should have 0 pruned chunks"
);
assert_eq!(
parsed["pruned_calls"], 0,
"Fresh index should have 0 pruned calls"
);
assert_eq!(parsed["hnsw_rebuilt"], false, "HNSW should not be rebuilt");
}
#[test]
#[serial]
fn test_gc_prunes_missing_files() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
fs::remove_file(dir.path().join("src/lib.rs")).expect("Failed to remove file");
let output = cqs()
.args(["gc", "--json"])
.current_dir(dir.path())
.assert()
.success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("Invalid JSON output: {} — raw: {}", e, stdout));
assert!(
parsed["pruned_chunks"].as_u64().unwrap() > 0,
"Should prune chunks for missing file"
);
assert_eq!(
parsed["missing_files"].as_u64().unwrap(),
1,
"Should report 1 missing file"
);
}
#[test]
fn test_dead_requires_index() {
let dir = TempDir::new().expect("Failed to create temp dir");
cqs()
.args(["dead"])
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("not found").or(predicate::str::contains("Index")));
}
#[test]
#[serial]
fn test_dead_json_output() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
let output = cqs()
.args(["dead", "--json"])
.current_dir(dir.path())
.assert()
.success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("Invalid JSON output: {} — raw: {}", e, stdout));
assert!(
parsed["dead"].is_array(),
"dead --json should have 'dead' array, got: {}",
parsed
);
assert!(
parsed["possibly_dead_pub"].is_array(),
"dead --json should have 'possibly_dead_pub' array, got: {}",
parsed
);
let possibly_dead = parsed["possibly_dead_pub"].as_array().unwrap();
assert!(
!possibly_dead.is_empty(),
"Public functions with no callers should be in possibly_dead_pub"
);
}
#[test]
#[serial]
fn test_dead_include_pub_flag() {
let dir = setup_project();
cqs()
.args(["init"])
.current_dir(dir.path())
.assert()
.success();
cqs()
.args(["index"])
.current_dir(dir.path())
.assert()
.success();
let output = cqs()
.args(["dead", "--include-pub", "--json"])
.current_dir(dir.path())
.assert()
.success();
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("Invalid JSON output: {} — raw: {}", e, stdout));
assert!(parsed["dead"].is_array(), "Should have 'dead' array");
let dead = parsed["dead"].as_array().unwrap();
assert!(
!dead.is_empty(),
"With --include-pub, public functions should be in 'dead' list"
);
}