use serde_json::{json, Value};
use std::fs;
use tempfile::TempDir;
use tokensave::mcp::handle_tool_call;
use tokensave::tokensave::TokenSave;
async fn setup_project() -> (TokenSave, TempDir) {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/main.rs"),
r#"
use crate::utils::helper;
mod utils;
fn main() {
let result = helper();
println!("{}", result);
}
"#,
)
.unwrap();
fs::write(
project.join("src/utils.rs"),
r#"
/// Returns a greeting string.
pub fn helper() -> String {
format_greeting("world")
}
fn format_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
"#,
)
.unwrap();
fs::create_dir_all(project.join("tests")).unwrap();
fs::write(
project.join("tests/test_utils.rs"),
r#"
use crate::utils::helper;
#[test]
fn test_helper() { assert!(!helper().is_empty()); }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
(cg, dir)
}
fn extract_text(value: &Value) -> &str {
value["content"][0]["text"]
.as_str()
.unwrap_or("<missing text>")
}
async fn find_node_id(cg: &TokenSave, name: &str) -> String {
let result = handle_tool_call(cg, "tokensave_search", json!({"query": name}), None)
.await
.unwrap();
let text = extract_text(&result.value);
let items: Vec<Value> = serde_json::from_str(text).unwrap();
items
.iter()
.find(|item| item["name"].as_str() == Some(name))
.unwrap_or_else(|| panic!("node '{}' not found via search", name))["id"]
.as_str()
.unwrap()
.to_string()
}
#[tokio::test]
async fn test_search() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper", "limit": 5}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
assert!(text.contains("helper"), "search results should contain 'helper'");
}
#[tokio::test]
async fn test_context() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_context",
json!({"task": "understand the helper function"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
}
#[tokio::test]
async fn test_callers() {
let (cg, _dir) = setup_project().await;
let node_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_callers",
json!({"node_id": node_id}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
}
#[tokio::test]
async fn test_callees() {
let (cg, _dir) = setup_project().await;
let node_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_callees",
json!({"node_id": node_id}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
}
#[tokio::test]
async fn test_impact() {
let (cg, _dir) = setup_project().await;
let node_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_impact",
json!({"node_id": node_id}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("node_count"));
}
#[tokio::test]
async fn test_node_existing() {
let (cg, _dir) = setup_project().await;
let node_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_node",
json!({"node_id": node_id}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("helper"), "node detail should contain the name");
assert!(text.contains("start_line"), "node detail should contain start_line");
assert!(text.contains("signature"), "node detail should contain signature");
assert!(text.contains("visibility"), "node detail should contain visibility");
}
#[tokio::test]
async fn test_node_not_found() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_node",
json!({"node_id": "nonexistent_id_12345"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("Node not found"),
"should report 'Node not found', got: {}",
text,
);
}
#[tokio::test]
async fn test_status() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_status",
json!({}),
Some(json!({"uptime": 100})),
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("node_count"), "status should include node_count");
assert!(text.contains("server"), "status should include server stats");
}
#[tokio::test]
async fn test_files_no_filter() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_files", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty(), "files listing should not be empty");
assert!(text.contains("indexed files"), "should have 'indexed files' header");
}
#[tokio::test]
async fn test_files_path_filter() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_files",
json!({"path": "src"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
assert!(
!text.contains("tests/test_utils"),
"path filter should exclude files outside 'src'"
);
}
#[tokio::test]
async fn test_files_pattern_filter() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_files",
json!({"pattern": "*.rs"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
}
#[tokio::test]
async fn test_files_flat_format() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_files",
json!({"format": "flat"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
assert!(text.contains("bytes"), "flat format should show byte sizes");
}
#[tokio::test]
async fn test_affected() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_affected",
json!({"files": ["src/utils.rs"]}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("affected_tests"), "should have affected_tests key");
assert!(text.contains("count"), "should have count key");
}
#[tokio::test]
async fn test_dead_code() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_dead_code", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("dead_code_count"), "should have dead_code_count key");
}
#[tokio::test]
async fn test_diff_context() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_diff_context",
json!({"files": ["src/utils.rs"]}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("changed_files"), "should have changed_files key");
assert!(text.contains("modified_symbols"), "should have modified_symbols key");
}
#[tokio::test]
async fn test_module_api() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_module_api",
json!({"path": "src"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("public_symbol_count"), "should have public_symbol_count key");
assert!(text.contains("helper"), "pub fn helper should appear in module API");
}
#[tokio::test]
async fn test_circular() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_circular", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("cycle_count"), "should have cycle_count key");
}
#[tokio::test]
async fn test_hotspots() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_hotspots",
json!({"limit": 5}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("hotspot_count"), "should have hotspot_count key");
}
#[tokio::test]
async fn test_similar() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_similar",
json!({"symbol": "helper"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
assert!(text.contains("helper"), "similar results should include 'helper'");
}
#[tokio::test]
async fn test_rename_preview() {
let (cg, _dir) = setup_project().await;
let node_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_rename_preview",
json!({"node_id": node_id}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("reference_count"), "should have reference_count key");
assert!(text.contains("node"), "should have node key");
}
#[tokio::test]
async fn test_unused_imports() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_unused_imports", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("unused_import_count"), "should have unused_import_count key");
}
#[tokio::test]
async fn test_rank() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_rank",
json!({"edge_kind": "calls", "direction": "incoming"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("ranking"), "should have ranking key");
assert!(text.contains("result_count"), "should have result_count key");
}
#[tokio::test]
async fn test_rank_invalid_direction() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_rank",
json!({"edge_kind": "calls", "direction": "sideways"}),
None,
)
.await;
match result {
Err(err) => {
let err_msg = format!("{}", err);
assert!(
err_msg.contains("invalid direction"),
"error should mention 'invalid direction', got: {}",
err_msg,
);
}
Ok(_) => panic!("invalid direction should produce an error"),
}
}
#[tokio::test]
async fn test_largest() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_largest",
json!({"limit": 5}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("ranking"), "should have ranking key");
assert!(text.contains("result_count"), "should have result_count key");
}
#[tokio::test]
async fn test_coupling() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_coupling",
json!({"direction": "fan_in"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("ranking"), "should have ranking key");
}
#[tokio::test]
async fn test_inheritance_depth() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_inheritance_depth",
json!({"limit": 5}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("result_count"), "should have result_count key");
}
#[tokio::test]
async fn test_distribution_default() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_distribution", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("per_file"), "default mode should be per_file");
}
#[tokio::test]
async fn test_distribution_summary() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_distribution",
json!({"summary": true}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("summary"), "summary mode should report 'summary'");
assert!(text.contains("distribution"), "should have distribution key");
}
#[tokio::test]
async fn test_recursion() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_recursion", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("cycle_count"), "should have cycle_count key");
}
#[tokio::test]
async fn test_complexity() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_complexity", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("ranking"), "should have ranking key");
assert!(text.contains("formula"), "should have formula key");
}
#[tokio::test]
async fn test_doc_coverage() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_doc_coverage", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("total_undocumented"), "should have total_undocumented key");
}
#[tokio::test]
async fn test_god_class() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_god_class",
json!({"limit": 5}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("result_count"), "should have result_count key");
}
#[tokio::test]
async fn test_changelog_no_git() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_changelog",
json!({"from_ref": "HEAD~1", "to_ref": "HEAD"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("git diff failed"),
"changelog on non-git dir should report git diff failure, got: {}",
text,
);
}
#[tokio::test]
async fn test_port_status() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_port_status",
json!({"source_dir": "src", "target_dir": "tests"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("coverage_percent"), "should have coverage_percent key");
}
#[tokio::test]
async fn test_port_order() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_port_order",
json!({"source_dir": "src"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("total_symbols"), "should have total_symbols key");
assert!(text.contains("levels"), "should have levels key");
}
#[tokio::test]
async fn test_unknown_tool() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_unknown", json!({}), None).await;
match result {
Err(err) => {
let err_msg = format!("{}", err);
assert!(
err_msg.contains("unknown tool"),
"error should mention 'unknown tool', got: {}",
err_msg,
);
}
Ok(_) => panic!("unknown tool should produce an error"),
}
}
#[tokio::test]
async fn test_missing_required_params() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_search", json!({}), None).await;
let err_msg = match result {
Err(err) => format!("{}", err),
Ok(_) => panic!("missing query should produce an error"),
};
assert!(
err_msg.contains("missing required parameter"),
"error should mention 'missing required parameter', got: {}",
err_msg,
);
}
#[tokio::test]
async fn test_node_id_alias() {
let (cg, _dir) = setup_project().await;
let node_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_node",
json!({"id": node_id}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("helper"),
"node lookup via 'id' alias should still find the node"
);
}
#[tokio::test]
async fn test_status_without_server_stats() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_status", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("node_count"), "status should include node_count");
assert!(
!text.contains("\"server\""),
"status without server_stats should not include 'server' key"
);
}
#[tokio::test]
async fn test_search_populates_touched_files() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper"}),
None,
)
.await
.unwrap();
assert!(
!result.touched_files.is_empty(),
"search results should populate touched_files"
);
}
#[tokio::test]
async fn test_rename_preview_not_found() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_rename_preview",
json!({"node_id": "nonexistent_id_12345"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("Node not found"),
"rename_preview with bad id should report 'Node not found', got: {}",
text,
);
}
#[tokio::test]
async fn test_coupling_fan_out() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_coupling",
json!({"direction": "fan_out"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("fan_out"), "should report fan_out direction");
}
#[tokio::test]
async fn test_rank_outgoing() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_rank",
json!({"edge_kind": "calls", "direction": "outgoing"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("outgoing"), "should reflect outgoing direction");
}
#[tokio::test]
async fn test_context_missing_task() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_context", json!({}), None).await;
assert!(result.is_err(), "context without task should error");
}
#[tokio::test]
async fn test_callers_missing_node_id() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_callers", json!({}), None).await;
assert!(result.is_err(), "callers without node_id should error");
}
#[tokio::test]
async fn test_affected_missing_files() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_affected", json!({}), None).await;
assert!(result.is_err(), "affected without files should error");
}
#[tokio::test]
async fn test_module_api_missing_path() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_module_api", json!({}), None).await;
assert!(result.is_err(), "module_api without path should error");
}
#[tokio::test]
async fn test_rank_missing_edge_kind() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_rank",
json!({"direction": "incoming"}),
None,
)
.await;
assert!(result.is_err(), "rank without edge_kind should error");
}
#[tokio::test]
async fn test_similar_missing_symbol() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_similar", json!({}), None).await;
assert!(result.is_err(), "similar without symbol should error");
}
#[tokio::test]
async fn test_diff_context_missing_files() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_diff_context", json!({}), None).await;
assert!(result.is_err(), "diff_context without files should error");
}
#[tokio::test]
async fn test_changelog_missing_refs() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_changelog", json!({}), None).await;
assert!(result.is_err(), "changelog without from_ref should error");
}
#[tokio::test]
async fn test_port_status_missing_dirs() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_port_status", json!({}), None).await;
assert!(result.is_err(), "port_status without source_dir should error");
}
#[tokio::test]
async fn test_port_order_missing_source_dir() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_port_order", json!({}), None).await;
assert!(result.is_err(), "port_order without source_dir should error");
}
#[tokio::test]
async fn test_changelog_with_real_git() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(project)
.output()
.expect("git init failed");
std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(project)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(project)
.output()
.unwrap();
fs::write(project.join("src/lib.rs"), "pub fn original() {}\n").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(project)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(project)
.output()
.unwrap();
fs::write(
project.join("src/lib.rs"),
"pub fn original() {}\npub fn added() {}\n",
)
.unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(project)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "add function"])
.current_dir(project)
.output()
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_changelog",
json!({"from_ref": "HEAD~1", "to_ref": "HEAD"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("git diff failed"),
"changelog in git repo should not fail, got: {}",
text,
);
assert!(
text.contains("changed_file_count") || text.contains("lib.rs"),
"changelog should mention changed files, got: {}",
text,
);
}
#[tokio::test]
async fn test_distribution_with_path_filter() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_distribution",
json!({"path": "src/"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("per_file"), "default mode should be per_file");
assert!(
!text.contains("tests/test_utils"),
"path filter should exclude files outside 'src/'",
);
}
#[tokio::test]
async fn test_files_grouped_format() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_files",
json!({"format": "grouped"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(!text.is_empty());
assert!(
text.contains("indexed files"),
"grouped format should have 'indexed files' header"
);
assert!(
text.contains("files)"),
"grouped format should show file counts per directory"
);
}
#[tokio::test]
async fn test_dead_code_custom_kinds() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_dead_code",
json!({"kinds": ["struct"]}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("dead_code_count"), "should have dead_code_count key");
let parsed: Value = serde_json::from_str(text).unwrap_or(json!({}));
if let Some(items) = parsed["dead_code"].as_array() {
for item in items {
assert_eq!(
item["kind"].as_str().unwrap_or(""),
"struct",
"dead code items should be structs when kinds=['struct']"
);
}
}
}
#[tokio::test]
async fn test_affected_with_custom_filter() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_affected",
json!({"files": ["src/utils.rs"], "filter": "**/*test*"}),
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(text.contains("affected_tests"), "should have affected_tests key");
assert!(text.contains("count"), "should have count key");
}
#[tokio::test]
async fn test_complexity_response_fields() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_complexity", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert!(parsed.get("ranking").is_some(), "should have ranking key");
assert!(parsed.get("formula").is_some(), "should have formula key");
if let Some(items) = parsed["ranking"].as_array() {
if let Some(first) = items.first() {
assert!(
first.get("cyclomatic_complexity").is_some(),
"ranking item should have cyclomatic_complexity"
);
assert!(
first.get("branches").is_some(),
"ranking item should have branches"
);
assert!(
first.get("max_nesting").is_some(),
"ranking item should have max_nesting"
);
assert!(
first.get("fan_out").is_some(),
"ranking item should have fan_out"
);
assert!(
first.get("score").is_some(),
"ranking item should have score"
);
}
}
}
#[tokio::test]
async fn test_doc_coverage_response_structure() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_doc_coverage", json!({}), None)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("total_undocumented").is_some(),
"should have total_undocumented"
);
assert!(
parsed.get("file_count").is_some(),
"should have file_count"
);
assert!(
parsed.get("files").is_some(),
"should have files array"
);
if let Some(files) = parsed["files"].as_array() {
if let Some(first) = files.first() {
assert!(first.get("file").is_some(), "file entry should have 'file'");
assert!(first.get("count").is_some(), "file entry should have 'count'");
assert!(first.get("symbols").is_some(), "file entry should have 'symbols'");
}
}
}