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, 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,
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,
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,
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,
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,
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,
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,
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})),
None,
)
.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, 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, 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,
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,
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,
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, 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,
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,
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, 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, 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,
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,
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, 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,
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,
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, 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,
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,
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, 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,
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, 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, 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, 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, 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,
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,
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,
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, 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, 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, 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, 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,
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,
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,
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,
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, 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, 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, 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, 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,
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, 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, 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, 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, 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, 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,
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,
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,
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,
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,
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, 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, 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'"
);
}
}
}
#[tokio::test]
async fn test_files_scope_prefix_filters() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_files", json!({}), None, Some("src"))
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("tests/"),
"scope_prefix 'src' should exclude test files"
);
assert!(text.contains("main.rs"), "should include src/main.rs");
}
#[tokio::test]
async fn test_search_scope_prefix_filters() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper", "limit": 20}),
None,
Some("tests"),
)
.await
.unwrap();
let text = extract_text(&result.value);
let items: Vec<serde_json::Value> = serde_json::from_str(text).unwrap_or_default();
for item in &items {
let file = item["file"].as_str().unwrap_or("");
assert!(
file.starts_with("tests"),
"scoped search should only return files under 'tests', got: {}",
file
);
}
}
#[tokio::test]
async fn test_files_explicit_path_overrides_scope() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_files",
json!({"path": "tests"}),
None,
Some("src"),
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("src/main.rs"),
"explicit path 'tests' should exclude src files"
);
}
#[tokio::test]
async fn test_context_scope_prefix_filters() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_context",
json!({"task": "understand helper"}),
None,
Some("tests"),
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.is_empty(),
"context should return results even when scoped"
);
}
#[tokio::test]
async fn test_status_reports_scope_prefix() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_status", json!({}), None, Some("src/mcp"))
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("scope_prefix"),
"status should report scope_prefix"
);
assert!(
text.contains("src/mcp"),
"status should show the actual prefix value"
);
}
#[tokio::test]
async fn test_status_no_scope_prefix() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_status", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("scope_prefix").is_none() || parsed["scope_prefix"].is_null(),
"status should not have scope_prefix when None"
);
}
#[tokio::test]
async fn test_str_replace_success() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/main.rs"),
"fn hello() {}\nfn world() {}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_str_replace",
json!({
"path": "src/main.rs",
"old_str": "fn hello() {}",
"new_str": "fn hello_updated() {}"
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["matched_str"], "fn hello() {}");
assert_eq!(parsed["new_str"], "fn hello_updated() {}");
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(content.contains("fn hello_updated() {}"));
assert!(!content.contains("fn hello() {}"));
}
#[tokio::test]
async fn test_str_replace_not_found() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/main.rs"), "fn hello() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_str_replace",
json!({
"path": "src/main.rs",
"old_str": "fn not_exists() {}",
"new_str": "fn replaced() {}"
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], false);
assert!(parsed["message"].as_str().unwrap().contains("not found"));
}
#[tokio::test]
async fn test_str_replace_multiple_matches_fails() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/main.rs"), "fn foo() {}\nfn foo() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_str_replace",
json!({
"path": "src/main.rs",
"old_str": "fn foo() {}",
"new_str": "fn bar() {}"
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], false);
assert!(parsed["message"]
.as_str()
.unwrap()
.contains("matches 2 times"));
}
#[tokio::test]
async fn test_multi_str_replace_success() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/main.rs"),
"fn foo() {}\nfn bar() {}\nfn baz() {}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_multi_str_replace",
json!({
"path": "src/main.rs",
"replacements": [
["fn foo() {}", "fn foo_replaced() {}"],
["fn bar() {}", "fn bar_replaced() {}"]
]
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["applied_count"], 2);
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(content.contains("fn foo_replaced()"));
assert!(content.contains("fn bar_replaced()"));
assert!(content.contains("fn baz() {}"));
}
#[tokio::test]
async fn test_multi_str_replace_atomic_failure() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/main.rs"), "fn foo() {}\nfn baz() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_multi_str_replace",
json!({
"path": "src/main.rs",
"replacements": [
["fn not_exists() {}", "fn replaced() {}"],
["fn baz() {}", "fn baz_replaced() {}"]
]
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], false);
assert!(parsed["message"]
.as_str()
.unwrap()
.contains("must match exactly once"));
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(content.contains("fn foo() {}"));
assert!(content.contains("fn baz() {}"));
assert!(!content.contains("fn replaced()"));
}
#[tokio::test]
async fn test_str_replace_unsupported_file_type_succeeds() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::write(project.join("style.css"), ".foo {\n\tfont-size: 14px;\n}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_str_replace",
json!({
"path": "style.css",
"old_str": "\tfont-size: 14px;",
"new_str": "\tfont-size: 0.85rem;"
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
let content = fs::read_to_string(project.join("style.css")).unwrap();
assert!(content.contains("0.85rem"));
assert!(!content.contains("14px"));
}
#[tokio::test]
async fn test_multi_str_replace_unsupported_file_type_succeeds() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::write(
project.join("style.css"),
".foo {\n\tfont-size: 14px;\n}\n.bar {\n\tfont-size: 16px;\n}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_multi_str_replace",
json!({
"path": "style.css",
"replacements": [
["\tfont-size: 14px;", "\tfont-size: 0.85rem;"],
["\tfont-size: 16px;", "\tfont-size: 1rem;"]
]
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["applied_count"], 2);
let content = fs::read_to_string(project.join("style.css")).unwrap();
assert!(content.contains("0.85rem"));
assert!(content.contains("1rem"));
assert!(!content.contains("14px"));
assert!(!content.contains("16px"));
}
#[tokio::test]
async fn test_insert_at_string_anchor_before() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/main.rs"),
"line one\nline two\nline three\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": "src/main.rs",
"anchor": "line two",
"content": "inserted line",
"before": true
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(
content.ends_with('\n'),
"trailing newline must be preserved"
);
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines[0], "line one");
assert_eq!(lines[1], "inserted line");
assert_eq!(lines[2], "line two");
assert_eq!(lines[3], "line three");
}
#[tokio::test]
async fn test_insert_at_line_number() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/main.rs"),
"line one\nline two\nline three\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": "src/main.rs",
"anchor": "2",
"content": "inserted at line 2",
"before": false
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["anchor_line"], 2);
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(
content.ends_with('\n'),
"trailing newline must be preserved"
);
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines[0], "line one");
assert_eq!(lines[1], "line two");
assert_eq!(lines[2], "inserted at line 2");
assert_eq!(lines[3], "line three");
}
#[tokio::test]
async fn test_insert_at_anchor_not_found() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/main.rs"), "line one\nline two\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": "src/main.rs",
"anchor": "nonexistent",
"content": "should not be inserted",
"before": true
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], false);
assert!(parsed["message"].as_str().unwrap().contains("not found"));
}
#[tokio::test]
async fn test_insert_at_ambiguous_anchor() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/main.rs"),
"line foo\nline foo\nline bar\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": "src/main.rs",
"anchor": "foo",
"content": "should not be inserted",
"before": true
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], false);
assert!(parsed["message"]
.as_str()
.unwrap()
.contains("matches 2 lines"));
}
#[tokio::test]
async fn test_insert_at_preserves_trailing_newline() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
let original = "fn hello() {}\n\nfn world() {}\n";
fs::write(project.join("src/lib.rs"), original).unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": "src/lib.rs",
"anchor": "fn world",
"content": "fn extra() {}",
"before": true
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["success"], true);
let content = fs::read_to_string(project.join("src/lib.rs")).unwrap();
assert!(
content.ends_with('\n'),
"file must end with newline after insert_at, got: {:?}",
&content[content.len().saturating_sub(20)..]
);
assert_eq!(content, "fn hello() {}\n\nfn extra() {}\nfn world() {}\n");
}
#[tokio::test]
async fn test_gini() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_gini",
json!({ "metric": "lines" }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("gini").is_some(),
"gini field should exist, got: {}",
text
);
assert!(
parsed.get("interpretation").is_some(),
"interpretation field should exist"
);
}
#[tokio::test]
async fn test_gini_default_metric() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_gini", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("gini").is_some(),
"gini field should exist with default args, got: {}",
text
);
}
#[tokio::test]
async fn test_dependency_depth() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_dependency_depth",
json!({ "limit": 5 }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("max_depth").is_some(),
"max_depth field should exist, got: {}",
text
);
assert!(
parsed.get("ideal_depth").is_some(),
"ideal_depth field should exist"
);
}
#[tokio::test]
async fn test_health_summary() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_health", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("quality_signal").is_some(),
"quality_signal field should exist, got: {}",
text
);
assert!(
parsed.get("files_analyzed").is_some(),
"files_analyzed field should exist"
);
}
#[tokio::test]
async fn test_health_detailed() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_health",
json!({ "details": true }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("quality_signal").is_some(),
"quality_signal should exist, got: {}",
text
);
let dims = parsed.get("dimensions").expect("dimensions should exist");
assert!(dims.get("acyclicity").is_some(), "acyclicity score missing");
assert!(dims.get("depth").is_some(), "depth score missing");
assert!(dims.get("equality").is_some(), "equality score missing");
assert!(dims.get("redundancy").is_some(), "redundancy score missing");
assert!(dims.get("modularity").is_some(), "modularity score missing");
}
#[tokio::test]
async fn test_dsm_stats() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_dsm",
json!({ "format": "stats" }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("files").is_some(),
"files field should exist, got: {}",
text
);
assert!(
parsed.get("density").is_some(),
"density field should exist"
);
}
#[tokio::test]
async fn test_dsm_clusters() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_dsm",
json!({ "format": "clusters" }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(
parsed.get("clusters").is_some(),
"clusters array should exist, got: {}",
text
);
}
#[tokio::test]
async fn test_test_risk() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_test_risk",
json!({ "limit": 10 }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
let summary = parsed.get("summary").expect("summary should exist");
assert!(
summary
.get("total_functions")
.and_then(|v| v.as_u64())
.is_some_and(|v| v > 0),
"total_functions should be > 0, got: {}",
text
);
assert!(parsed.get("risks").is_some(), "risks array should exist");
}
#[tokio::test]
async fn test_session_start() {
let (cg, dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_session_start", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(output["quality_signal"].as_u64().is_some());
assert_eq!(output["status"].as_str().unwrap(), "baseline_saved");
let baseline_path = dir.path().join(".tokensave/session_baseline.json");
assert!(baseline_path.exists(), "baseline file should exist");
}
#[tokio::test]
async fn test_session_end() {
let (cg, dir) = setup_project().await;
handle_tool_call(&cg, "tokensave_session_start", json!({}), None, None)
.await
.unwrap();
let result = handle_tool_call(&cg, "tokensave_session_end", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(output["signal_before"].as_u64().is_some());
assert!(output["signal_after"].as_u64().is_some());
assert!(output["delta"].is_number());
let baseline_path = dir.path().join(".tokensave/session_baseline.json");
assert!(
!baseline_path.exists(),
"baseline should be removed after session_end"
);
}
#[tokio::test]
async fn test_session_end_no_baseline() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_session_end", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(output["status"].as_str().unwrap(), "no_baseline");
}
#[tokio::test]
async fn test_body_returns_full_function_source() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_body",
json!({"symbol": "format_greeting"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["match_count"].as_u64().unwrap(), 1);
let body = output["matches"][0]["body"].as_str().unwrap();
assert!(
body.contains("fn format_greeting"),
"body should contain the function signature, got: {body}"
);
assert!(
body.contains("Hello"),
"body should contain the function body, got: {body}"
);
}
#[tokio::test]
async fn test_body_unknown_symbol() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_body",
json!({"symbol": "no_such_symbol_anywhere"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("No symbol named"),
"should report no match, got: {text}"
);
}
#[tokio::test]
async fn test_body_missing_symbol_param() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_body", json!({}), None, None).await;
assert!(result.is_err(), "should error when symbol is missing");
}
#[tokio::test]
async fn test_todos_finds_markers() {
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#"
fn main() {
// TODO: refactor this
let x = 1;
// FIXME: handle the error case
let y = 2;
println!("{} {}", x, y);
}
fn helper() {
// not a marker: rendered todoist
let _ = 0;
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_todos", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let count = output["match_count"].as_u64().unwrap();
assert_eq!(count, 2, "should find exactly TODO and FIXME, got: {text}");
let kinds: Vec<&str> = output["markers"]
.as_array()
.unwrap()
.iter()
.map(|m| m["kind"].as_str().unwrap())
.collect();
assert!(kinds.contains(&"TODO"));
assert!(kinds.contains(&"FIXME"));
let enclosing: Vec<&str> = output["markers"]
.as_array()
.unwrap()
.iter()
.filter_map(|m| m["enclosing"].as_str())
.collect();
assert!(
enclosing.iter().any(|e| e.contains("main")),
"TODO inside main should report main as enclosing, got: {enclosing:?}"
);
}
#[tokio::test]
async fn test_todos_filters_by_kind() {
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#"
fn main() {
// TODO: a
// FIXME: b
// HACK: c
let _ = 0;
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_todos",
json!({"kinds": ["FIXME"]}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["match_count"].as_u64().unwrap(), 1);
assert_eq!(output["markers"][0]["kind"].as_str().unwrap(), "FIXME");
}
#[tokio::test]
async fn test_todos_empty_when_clean() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_todos", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["match_count"].as_u64().unwrap(), 0);
}
#[tokio::test]
async fn test_callers_for_returns_caller_set_per_id() {
let (cg, _dir) = setup_project().await;
let helper_id = find_node_id(&cg, "helper").await;
let format_id = find_node_id(&cg, "format_greeting").await;
let result = handle_tool_call(
&cg,
"tokensave_callers_for",
json!({"node_ids": [helper_id.clone(), format_id.clone()]}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["truncated"], json!(false));
assert!(output["max_per_item"].as_u64().unwrap() > 0);
let callers = &output["callers"];
let helper_callers = callers[&helper_id].as_array().unwrap();
let format_callers = callers[&format_id].as_array().unwrap();
assert!(
!helper_callers.is_empty(),
"expected helper to have at least one caller"
);
assert!(
!format_callers.is_empty(),
"expected format_greeting to have at least one caller"
);
}
#[tokio::test]
async fn test_callers_for_includes_unmatched_ids_as_empty() {
let (cg, _dir) = setup_project().await;
let helper_id = find_node_id(&cg, "helper").await;
let bogus_id = "function:0000000000000000000000000000ffff".to_string();
let result = handle_tool_call(
&cg,
"tokensave_callers_for",
json!({"node_ids": [helper_id.clone(), bogus_id.clone()]}),
None,
None,
)
.await
.unwrap();
let output: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
let callers = &output["callers"];
assert!(callers[&bogus_id].as_array().unwrap().is_empty());
assert!(!callers[&helper_id].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn test_callers_for_respects_max_per_item() {
let (cg, _dir) = setup_project().await;
let helper_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_callers_for",
json!({"node_ids": [helper_id.clone()], "max_per_item": 0}),
None,
None,
)
.await
.unwrap();
let output: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(output["truncated"], json!(true));
assert!(output["callers"][&helper_id].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn test_callers_for_rejects_empty_input() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_callers_for",
json!({"node_ids": []}),
None,
None,
)
.await;
let Err(err) = result else {
panic!("expected error for empty node_ids");
};
assert!(format!("{err}").contains("non-empty"));
}
#[tokio::test]
async fn test_callers_for_rejects_unknown_kind() {
let (cg, _dir) = setup_project().await;
let helper_id = find_node_id(&cg, "helper").await;
let result = handle_tool_call(
&cg,
"tokensave_callers_for",
json!({"node_ids": [helper_id], "kind": "not_a_real_kind"}),
None,
None,
)
.await;
let Err(err) = result else {
panic!("expected error for unknown edge kind");
};
assert!(format!("{err}").contains("unknown edge kind"));
}
#[tokio::test]
async fn test_by_qualified_name_finds_indexed_node() {
let (cg, _dir) = setup_project().await;
let helper = cg
.get_node(&find_node_id(&cg, "helper").await)
.await
.unwrap()
.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_by_qualified_name",
json!({"qualified_name": helper.qualified_name}),
None,
None,
)
.await
.unwrap();
let items: Vec<Value> = serde_json::from_str(extract_text(&result.value)).unwrap();
assert!(
!items.is_empty(),
"expected at least one match for helper qname"
);
assert!(items.iter().any(|i| i["name"] == "helper"));
assert!(items[0].get("attrs_start_line").is_some());
}
#[tokio::test]
async fn test_by_qualified_name_returns_empty_for_unknown() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_by_qualified_name",
json!({"qualified_name": "crate::does::not::exist"}),
None,
None,
)
.await
.unwrap();
let items: Vec<Value> = serde_json::from_str(extract_text(&result.value)).unwrap();
assert!(items.is_empty());
}
#[tokio::test]
async fn test_by_qualified_name_requires_param() {
let (cg, _dir) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_by_qualified_name", json!({}), None, None).await;
let Err(err) = result else {
panic!("expected error when qualified_name is missing");
};
assert!(format!("{err}").contains("qualified_name"));
}