#![cfg_attr(coverage_nightly, coverage(off))]
include!("core_tools.rs");
include!("extended_tools.rs");
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::path::Path;
fn mkdir(p: &Path) {
std::fs::create_dir_all(p).expect("mkdir");
}
fn touch(p: &Path) {
if let Some(parent) = p.parent() {
mkdir(parent);
}
std::fs::write(p, "").expect("touch");
}
#[test]
fn test_parse_complexity_args_valid() {
let args = parse_complexity_args(json!({
"project_path": "/tmp",
"toolchain": "rust",
"max_cyclomatic": 10,
}))
.expect("valid parse");
assert_eq!(args.project_path.as_deref(), Some("/tmp"));
assert_eq!(args.toolchain.as_deref(), Some("rust"));
assert_eq!(args.max_cyclomatic, Some(10));
}
#[test]
fn test_parse_complexity_args_empty_object_all_none() {
let args = parse_complexity_args(json!({})).expect("empty object is valid");
assert!(args.project_path.is_none());
assert!(args.toolchain.is_none());
assert!(args.max_cyclomatic.is_none());
}
#[test]
fn test_parse_complexity_args_wrong_types_return_err() {
let result = parse_complexity_args(json!({"max_cyclomatic": "not-a-number"}));
let err = result.unwrap_err();
assert!(err.contains("Invalid analyze_complexity arguments"));
}
#[test]
fn test_detect_toolchain_explicit_arg_wins() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("Cargo.toml"));
let t = detect_toolchain(&Some("deno".to_string()), tmp.path());
assert_eq!(t, "deno");
}
#[test]
fn test_detect_toolchain_detects_rust_from_cargo_toml() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("Cargo.toml"));
assert_eq!(detect_toolchain(&None, tmp.path()), "rust");
}
#[test]
fn test_detect_toolchain_detects_deno_from_package_json() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("package.json"));
assert_eq!(detect_toolchain(&None, tmp.path()), "deno");
}
#[test]
fn test_detect_toolchain_detects_deno_from_deno_json() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("deno.json"));
assert_eq!(detect_toolchain(&None, tmp.path()), "deno");
}
#[test]
fn test_detect_toolchain_detects_python_uv_from_pyproject() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("pyproject.toml"));
assert_eq!(detect_toolchain(&None, tmp.path()), "python-uv");
}
#[test]
fn test_detect_toolchain_detects_python_uv_from_requirements_txt() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("requirements.txt"));
assert_eq!(detect_toolchain(&None, tmp.path()), "python-uv");
}
#[test]
fn test_detect_toolchain_defaults_to_rust() {
let tmp = tempfile::TempDir::new().unwrap();
assert_eq!(detect_toolchain(&None, tmp.path()), "rust");
}
#[test]
fn test_build_complexity_thresholds_defaults_when_none() {
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: None,
max_cyclomatic: None,
max_cognitive: None,
include: None,
top_files: None,
};
let t = build_complexity_thresholds(&args);
assert!(t.cyclomatic_error > 0);
assert!(t.cognitive_error > 0);
}
#[test]
fn test_build_complexity_thresholds_sets_cyclomatic() {
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: None,
max_cyclomatic: Some(20),
max_cognitive: None,
include: None,
top_files: None,
};
let t = build_complexity_thresholds(&args);
assert_eq!(t.cyclomatic_error, 20);
assert_eq!(t.cyclomatic_warn, 15); }
#[test]
fn test_build_complexity_thresholds_sets_cognitive() {
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: None,
max_cyclomatic: None,
max_cognitive: Some(12),
include: None,
top_files: None,
};
let t = build_complexity_thresholds(&args);
assert_eq!(t.cognitive_error, 12);
assert_eq!(t.cognitive_warn, 9); }
#[test]
fn test_build_complexity_thresholds_min_warn_is_one() {
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: None,
max_cyclomatic: Some(1),
max_cognitive: Some(1),
include: None,
top_files: None,
};
let t = build_complexity_thresholds(&args);
assert_eq!(t.cyclomatic_warn, 1);
assert_eq!(t.cognitive_warn, 1);
}
#[test]
fn test_should_analyze_file_rust_only_rs() {
assert!(should_analyze_file(Path::new("main.rs"), "rust"));
assert!(!should_analyze_file(Path::new("main.ts"), "rust"));
assert!(!should_analyze_file(Path::new("main"), "rust"));
}
#[test]
fn test_should_analyze_file_deno_accepts_ts_tsx_js_jsx() {
for ext in ["ts", "tsx", "js", "jsx"] {
let path = format!("file.{ext}");
assert!(
should_analyze_file(Path::new(&path), "deno"),
"failed for {ext}"
);
}
assert!(!should_analyze_file(Path::new("file.py"), "deno"));
}
#[test]
fn test_should_analyze_file_python_uv_only_py() {
assert!(should_analyze_file(Path::new("x.py"), "python-uv"));
assert!(!should_analyze_file(Path::new("x.rs"), "python-uv"));
}
#[test]
fn test_should_analyze_file_unknown_toolchain_rejects_all() {
assert!(!should_analyze_file(Path::new("x.rs"), "unknown-toolchain"));
assert!(!should_analyze_file(Path::new("x.py"), ""));
}
#[test]
fn test_matches_include_filters_none_returns_true() {
assert!(matches_include_filters(Path::new("src/main.rs"), &None));
}
#[test]
fn test_matches_include_filters_empty_vec_returns_true() {
assert!(matches_include_filters(
Path::new("src/main.rs"),
&Some(vec![])
));
}
#[test]
fn test_matches_include_filters_any_pattern_matches() {
let patterns = Some(vec!["*.rs".to_string(), "lib".to_string()]);
assert!(matches_include_filters(Path::new("src/main.rs"), &patterns));
}
#[test]
fn test_matches_include_filters_no_pattern_matches_is_false() {
let patterns = Some(vec!["*.py".to_string(), "*.ts".to_string()]);
assert!(!matches_include_filters(
Path::new("src/main.rs"),
&patterns
));
}
#[test]
fn test_matches_pattern_double_star_matches_trailing_suffix() {
assert!(matches_pattern("src/a/b/c.rs", "src/**/c.rs"));
}
#[test]
fn test_matches_pattern_double_star_with_three_parts_returns_false() {
assert!(!matches_pattern("a/b/c", "a/**/b/**"));
}
#[test]
fn test_matches_pattern_extension_wildcard() {
assert!(matches_pattern("src/main.rs", "*.rs"));
assert!(!matches_pattern("src/main.ts", "*.rs"));
}
#[test]
fn test_matches_pattern_substring_fallback() {
assert!(matches_pattern("path/to/my-handler.rs", "my-handler"));
assert!(!matches_pattern("path/to/other.rs", "my-handler"));
}
fn empty_report() -> crate::services::complexity::ComplexityReport {
crate::services::complexity::aggregate_results(Vec::new())
}
fn args_with_format(fmt: &str) -> AnalyzeComplexityArgs {
AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: Some(fmt.to_string()),
max_cyclomatic: None,
max_cognitive: None,
include: None,
top_files: None,
}
}
#[test]
fn test_format_complexity_output_json_returns_valid_json() {
let rpt = empty_report();
let out = format_complexity_output(&rpt, &args_with_format("json"));
let _: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
}
#[test]
fn test_format_complexity_output_sarif_contains_recognizable_content() {
let rpt = empty_report();
let out = format_complexity_output(&rpt, &args_with_format("sarif"));
assert!(!out.is_empty());
}
#[test]
fn test_format_complexity_output_full_returns_non_empty() {
let rpt = empty_report();
let out = format_complexity_output(&rpt, &args_with_format("full"));
assert!(!out.is_empty());
}
#[test]
fn test_format_complexity_output_default_summary_when_no_format() {
let rpt = empty_report();
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: None,
max_cyclomatic: None,
max_cognitive: None,
include: None,
top_files: None,
};
let out = format_complexity_output(&rpt, &args);
assert!(!out.is_empty());
}
#[test]
fn test_generate_complexity_content_top_files_zero_falls_back_to_summary() {
let rpt = empty_report();
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: Some("summary".into()),
max_cyclomatic: None,
max_cognitive: None,
include: None,
top_files: Some(0),
};
let out = generate_complexity_content(&rpt, &[], &args);
assert!(!out.is_empty());
}
#[test]
fn test_generate_complexity_content_no_top_files_uses_summary() {
let rpt = empty_report();
let args = args_with_format("summary");
let out = generate_complexity_content(&rpt, &[], &args);
assert!(!out.is_empty());
}
#[test]
fn test_generate_complexity_content_top_files_nonzero_uses_rankings() {
let rpt = empty_report();
let args = AnalyzeComplexityArgs {
project_path: None,
toolchain: None,
format: Some("summary".into()),
max_cyclomatic: None,
max_cognitive: None,
include: None,
top_files: Some(5),
};
let out = generate_complexity_content(&rpt, &[], &args);
assert!(out.contains("Top 0") || out.contains("Top"));
}
#[test]
fn test_format_complexity_rankings_json_output_has_rankings_key() {
let args = args_with_format("json");
let rankings: Vec<(String, crate::services::ranking::CompositeComplexityScore)> = vec![];
let out = format_complexity_rankings(&rankings, &args);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert!(v.get("rankings").is_some());
assert!(v.get("analysis_type").is_some());
}
#[test]
fn test_format_complexity_rankings_table_output_has_header() {
let args = args_with_format("summary");
let rankings: Vec<(String, crate::services::ranking::CompositeComplexityScore)> = vec![];
let out = format_complexity_rankings(&rankings, &args);
assert!(out.contains("Top 0 Complexity Files"));
assert!(out.contains("| Rank |"));
}
#[test]
fn test_resolve_project_path_complexity_none_is_err() {
let err = resolve_project_path_complexity(None).unwrap_err();
assert!(!err.is_empty());
}
#[test]
fn test_resolve_project_path_complexity_empty_string_is_err() {
let err = resolve_project_path_complexity(Some(String::new())).unwrap_err();
assert!(!err.is_empty());
}
}
#[cfg(test)]
mod r22_1_d101_cwd_guard_tests {
use super::*;
use serde_json::json;
fn assert_invalid_params(response: &McpResponse, context: &str) {
assert!(
response.error.is_some(),
"{context}: expected an error response, got success: {response:?}"
);
let err = response.error.as_ref().unwrap();
assert_eq!(
err.code, -32602,
"{context}: expected JSON-RPC -32602 (Invalid params), got {}: message={}",
err.code, err.message
);
assert!(
err.message.contains("project_path"),
"{context}: error message should name the offending field: {}",
err.message
);
}
#[tokio::test]
async fn analyze_complexity_rejects_missing_project_path() {
let response = handle_analyze_complexity(json!(1), json!({})).await;
assert_invalid_params(&response, "analyze_complexity / missing");
}
#[tokio::test]
async fn analyze_complexity_rejects_null_project_path() {
let response = handle_analyze_complexity(json!(1), json!({ "project_path": null })).await;
assert_invalid_params(&response, "analyze_complexity / null");
}
#[tokio::test]
async fn analyze_complexity_rejects_empty_project_path() {
let response = handle_analyze_complexity(json!(1), json!({ "project_path": "" })).await;
assert_invalid_params(&response, "analyze_complexity / empty");
}
#[tokio::test]
async fn analyze_complexity_rejects_whitespace_project_path() {
let response = handle_analyze_complexity(json!(1), json!({ "project_path": " " })).await;
assert_invalid_params(&response, "analyze_complexity / whitespace");
}
#[tokio::test]
async fn analyze_code_churn_rejects_missing_project_path() {
let response = handle_analyze_code_churn(json!(1), json!({})).await;
assert_invalid_params(&response, "analyze_code_churn / missing");
}
#[tokio::test]
async fn analyze_code_churn_rejects_empty_project_path() {
let response = handle_analyze_code_churn(json!(1), json!({ "project_path": "" })).await;
assert_invalid_params(&response, "analyze_code_churn / empty");
}
#[tokio::test]
async fn analyze_dag_rejects_missing_project_path() {
let response = handle_analyze_dag(json!(1), json!({})).await;
assert_invalid_params(&response, "analyze_dag / missing");
}
#[tokio::test]
async fn analyze_dag_rejects_empty_project_path() {
let response = handle_analyze_dag(json!(1), json!({ "project_path": "" })).await;
assert_invalid_params(&response, "analyze_dag / empty");
}
#[tokio::test]
async fn generate_context_rejects_missing_project_path() {
let response = handle_generate_context(json!(1), json!({})).await;
assert_invalid_params(&response, "generate_context / missing");
}
#[tokio::test]
async fn generate_context_rejects_empty_project_path() {
let response = handle_generate_context(json!(1), json!({ "project_path": "" })).await;
assert_invalid_params(&response, "generate_context / empty");
}
#[tokio::test]
async fn analyze_system_architecture_rejects_missing_project_path() {
let response = handle_analyze_system_architecture(json!(1), json!({})).await;
assert_invalid_params(&response, "analyze_system_architecture / missing");
}
#[tokio::test]
async fn analyze_system_architecture_rejects_empty_project_path() {
let response =
handle_analyze_system_architecture(json!(1), json!({ "project_path": "" })).await;
assert_invalid_params(&response, "analyze_system_architecture / empty");
}
#[test]
fn require_project_path_accepts_nonempty() {
let out = require_project_path(Some("/tmp/x".to_string())).unwrap();
assert_eq!(out, std::path::PathBuf::from("/tmp/x"));
}
#[test]
fn require_project_path_rejects_none() {
let err = require_project_path(None).unwrap_err();
assert!(err.contains("project_path"), "{err}");
assert!(err.contains("D101"), "{err}");
}
#[test]
fn require_project_path_rejects_empty() {
let err = require_project_path(Some(String::new())).unwrap_err();
assert!(err.contains("non-empty"), "{err}");
}
#[test]
fn require_project_path_rejects_whitespace() {
let err = require_project_path(Some(" \n\t ".to_string())).unwrap_err();
assert!(err.contains("non-empty"), "{err}");
}
}