use serde_json::{json, Value};
use std::fs;
use tempfile::TempDir;
use tokensave::config::{load_config, save_config};
use tokensave::mcp::handle_tool_call;
use tokensave::tokensave::TokenSave;
async fn setup_project() -> (TempDir, TokenSave) {
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();
(dir, cg)
}
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 (_dir, cg) = 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_search_literal_finds_string_in_body() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "Hello, {}!", "literal": true}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert_eq!(
parsed["literal"], true,
"result must mark that it was a literal search"
);
let matches = parsed["matches"].as_array().expect("matches array");
assert_eq!(matches.len(), 1, "expected exactly one literal match");
let m = &matches[0];
assert_eq!(m["file"], "src/utils.rs");
assert_eq!(m["line"], 8); assert!(
m["text"].as_str().unwrap().contains("Hello, {}!"),
"matched line text should contain the query"
);
assert_eq!(
m["enclosing"], "format_greeting",
"match should map to its enclosing symbol"
);
}
#[tokio::test]
async fn test_search_literal_respects_queryignore() {
let (_dir, cg) = setup_project().await;
let ts_dir = cg.project_root().join(".tokensave");
std::fs::create_dir_all(&ts_dir).unwrap();
std::fs::write(ts_dir.join("queryignore"), "utils\n").unwrap();
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "Hello, {}!", "literal": true}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert_eq!(
parsed["count"], 0,
"queryignore-suppressed file must not appear in literal matches: {parsed}"
);
}
#[tokio::test]
async fn test_search_literal_no_match_returns_empty() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "this string does not exist anywhere zzz", "literal": true}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["literal"], true);
assert!(parsed["matches"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn test_search_literal_respects_limit() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper", "literal": true, "limit": 1}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["matches"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn test_search_literal_respects_path_include() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({
"query": "helper",
"literal": true,
"path_include": ["src/utils.rs"],
"limit": 20,
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
let matches = parsed["matches"].as_array().expect("matches array");
assert!(
!matches.is_empty(),
"expected at least one match in src/utils.rs"
);
for m in matches {
assert_eq!(
m["file"], "src/utils.rs",
"every literal match must be inside the path_include target, got: {m}"
);
}
}
#[tokio::test]
async fn test_search_literal_respects_path_exclude() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({
"query": "helper",
"literal": true,
"path_exclude": ["tests/"],
"limit": 20,
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
let matches = parsed["matches"].as_array().expect("matches array");
assert!(!matches.is_empty(), "expected matches outside tests/");
for m in matches {
assert!(
!m["file"].as_str().unwrap().contains("tests/"),
"path_exclude should have dropped this match: {m}"
);
}
}
#[tokio::test]
async fn test_search_literal_case_sensitive() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "hello, {}!", "literal": true}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: Value = serde_json::from_str(text).unwrap();
assert!(
parsed["matches"].as_array().unwrap().is_empty(),
"literal search must be case-sensitive"
);
}
#[tokio::test]
async fn test_context() {
let (_dir, cg) = 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 (_dir, cg) = 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_callers_nonexistent_node_id_errors() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_callers",
json!({"node_id": "helper"}),
None,
None,
)
.await;
let Err(err) = result else {
panic!("expected an error for a nonexistent node_id")
};
let msg = format!("{err}");
assert!(
msg.contains("node not found"),
"expected a node-not-found error, got: {msg}"
);
}
#[tokio::test]
async fn test_callees() {
let (_dir, cg) = 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_callees_nonexistent_node_id_errors() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_callees",
json!({"node_id": "helper"}),
None,
None,
)
.await;
let Err(err) = result else {
panic!("expected an error for a nonexistent node_id")
};
let msg = format!("{err}");
assert!(
msg.contains("node not found"),
"expected a node-not-found error, got: {msg}"
);
}
#[tokio::test]
async fn test_impact() {
let (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 port_status_does_not_match_methods_of_different_parents() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src_a")).unwrap();
fs::create_dir_all(project.join("src_b")).unwrap();
fs::write(
project.join("src_a/biquad.rs"),
"pub struct Biquad;\n\
impl Biquad {\n pub fn new() -> Self { Self }\n pub fn process(&self) {}\n}\n",
)
.unwrap();
fs::write(
project.join("src_b/adaa.rs"),
"pub struct Adaa;\n\
impl Adaa {\n pub fn new() -> Self { Self }\n pub fn process(&self) {}\n}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_port_status",
json!({
"source_dir": "src_a",
"target_dir": "src_b",
"kinds": ["method"],
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).expect("response must be JSON");
let matched: Vec<&Value> = output["matched_symbols"]
.as_array()
.map(|a| a.iter().collect())
.unwrap_or_default();
assert!(
matched.is_empty(),
"Biquad::* and Adaa::* must not cross-match — got matches: {matched:?}"
);
assert_eq!(
output["matched"].as_u64(),
Some(0),
"matched count must be 0; output={output}"
);
}
#[tokio::test]
async fn port_status_matches_methods_with_same_parent_type() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src_a")).unwrap();
fs::create_dir_all(project.join("src_b")).unwrap();
fs::write(
project.join("src_a/biquad.rs"),
"pub struct Biquad;\n\
impl Biquad { pub fn process(&self) {} }\n",
)
.unwrap();
fs::write(
project.join("src_b/biquad_port.rs"),
"pub struct Biquad;\n\
impl Biquad { pub fn process(&self) {} }\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_port_status",
json!({
"source_dir": "src_a",
"target_dir": "src_b",
"kinds": ["method"],
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).expect("response must be JSON");
assert_eq!(
output["matched"].as_u64(),
Some(1),
"Biquad::process should match Biquad::process; output={output}"
);
}
#[tokio::test]
async fn test_port_order() {
let (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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_multi_str_replace_unicode_preview_does_not_panic() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
let original = "fn main() {}\n";
fs::write(project.join("src/main.rs"), original).unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let missing_old = format!("{}é", "a".repeat(19));
let result = handle_tool_call(
&cg,
"tokensave_multi_str_replace",
json!({
"path": "src/main.rs",
"replacements": [
[missing_old, "replacement"]
]
}),
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);
let message = parsed["message"].as_str().unwrap();
assert!(message.contains("matches 0 times"));
assert!(message.contains("must match exactly once"));
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert_eq!(content, original);
}
#[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 ast_grep_rewrite_has_literal_fallback_when_binary_missing() {
if tokensave::mcp::tools::ast_grep_available() {
return;
}
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub fn old_name() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_ast_grep_rewrite",
json!({"path": "src/lib.rs", "pattern": "old_name", "rewrite": "new_name"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["success"].as_bool(), Some(true), "{output}");
assert!(
fs::read_to_string(project.join("src/lib.rs"))
.unwrap()
.contains("new_name"),
"literal fallback should update the file"
);
}
#[tokio::test]
async fn ast_grep_rewrite_uses_current_cli_update_flag() {
if !tokensave::mcp::tools::ast_grep_available() {
return;
}
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
"pub fn caller() { old_name(); }\npub fn old_name() {}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_ast_grep_rewrite",
json!({"path": "src/lib.rs", "pattern": "old_name()", "rewrite": "new_name()"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["success"].as_bool(), Some(true), "{output}");
let content = fs::read_to_string(project.join("src/lib.rs")).unwrap();
assert!(
content.contains("new_name();"),
"ast-grep rewrite should apply with the installed CLI: {content}"
);
assert!(
!output["message"]
.as_str()
.unwrap_or_default()
.contains("unexpected argument '-d'"),
"rewrite must not use the removed -d flag: {output}"
);
}
#[tokio::test]
async fn branch_diff_returns_empty_when_base_equals_head() {
let (_dir, cg) = setup_project().await;
let tokensave_dir = tokensave::config::get_tokensave_dir(cg.project_root());
let meta = tokensave::branch_meta::BranchMeta::new("master");
tokensave::branch_meta::save_branch_meta(&tokensave_dir, &meta).unwrap();
let result = handle_tool_call(
&cg,
"tokensave_branch_diff",
json!({"base": "master", "head": "master"}),
None,
None,
)
.await
.expect("branch_diff must not error when base == head");
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).expect("response must be valid JSON");
assert_eq!(output["summary"]["added"].as_u64(), Some(0));
assert_eq!(output["summary"]["removed"].as_u64(), Some(0));
assert_eq!(output["summary"]["changed"].as_u64(), Some(0));
assert_eq!(output["added"].as_array().map(Vec::len), Some(0));
assert_eq!(output["removed"].as_array().map(Vec::len), Some(0));
assert_eq!(output["changed"].as_array().map(Vec::len), Some(0));
}
#[tokio::test]
async fn ast_grep_rewrite_surfaces_useful_error_on_empty_stderr() {
if !tokensave::mcp::tools::ast_grep_available() {
return;
}
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub fn foo() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_ast_grep_rewrite",
json!({
"path": "src/lib.rs",
"pattern": "__NONEXISTENT_PATTERN__",
"rewrite": "whatever"
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(output["success"].as_bool(), Some(false), "{output}");
let message = output["message"].as_str().unwrap_or_default();
assert!(
!message.trim_end_matches(':').trim().eq("ast-grep failed"),
"message must not end as an empty 'ast-grep failed:' — got: {message:?}"
);
assert!(
message.contains("exit") || message.contains("0 nodes") || message.contains("no language"),
"message must explain the likely cause (exit code / no language / 0 matches), got: {message:?}"
);
}
#[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_unicode_anchor_prefix_does_not_panic() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
let original = "line one\nline two\n";
fs::write(project.join("src/main.rs"), original).unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let long_anchor = format!("{}é", "a".repeat(99));
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": "src/main.rs",
"anchor": long_anchor,
"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"));
let content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert_eq!(content, original);
}
#[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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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_redundancy_finds_planted_duplicate() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub fn compute_a(value: i32) -> i32 {
let mut acc = 0;
for i in 0..value {
if i % 2 == 0 {
acc += i;
} else {
acc -= i;
}
}
acc
}
pub fn compute_b(input: i32) -> i32 {
let mut total = 0;
for j in 0..input {
if j % 2 == 0 {
total += j;
} else {
total -= j;
}
}
total
}
pub fn unrelated(x: i32) -> i32 {
x * 2
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_redundancy",
json!({ "min_lines": 5, "similarity_threshold": 0.5 }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
let pair_count = parsed["pair_count"].as_u64().unwrap_or(0);
assert!(
pair_count >= 1,
"expected at least 1 duplicate pair, got: {text}"
);
let pairs = parsed["pairs"].as_array().expect("pairs array");
let top = &pairs[0];
let kind = top["overlap_kind"].as_str().unwrap_or("");
assert_eq!(
kind, "ast_isomorphic",
"top pair should be AST-isomorphic; full output: {text}"
);
let severity = top["severity"].as_str().unwrap_or("");
assert_eq!(
severity, "definite",
"AST-identical pair should be 'definite'"
);
let names: Vec<&str> = vec![
top["a"]["name"].as_str().unwrap_or(""),
top["b"]["name"].as_str().unwrap_or(""),
];
assert!(
names.contains(&"compute_a") && names.contains(&"compute_b"),
"expected compute_a/compute_b in pair, got {names:?}"
);
let result2 = handle_tool_call(
&cg,
"tokensave_redundancy",
json!({ "min_lines": 5, "similarity_threshold": 0.5 }),
None,
None,
)
.await
.unwrap();
let parsed2: serde_json::Value = serde_json::from_str(extract_text(&result2.value)).unwrap();
assert_eq!(parsed2["pair_count"], parsed["pair_count"]);
}
#[tokio::test]
async fn test_runtime_snapshot_exposes_process_and_db_signals() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_runtime", 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("captured_at").is_some());
assert!(parsed["tokensave_version"].is_string());
assert!(parsed["host_os"].is_string());
let proc = &parsed["process"];
assert_eq!(
proc["pid"].as_u64().unwrap_or(0),
u64::from(std::process::id()),
"snapshot must report this process's PID"
);
assert!(
proc["rss_bytes"].as_u64().unwrap_or(0) > 0,
"RSS should be non-zero"
);
assert!(proc["system_cpu_count"].as_u64().unwrap_or(0) >= 1);
assert!(proc["system_total_memory_bytes"].as_u64().unwrap_or(0) > 0);
let db = &parsed["database"];
assert!(db["db_path"].is_string());
assert!(
db["db_size_bytes"].as_u64().unwrap_or(0) > 0,
"DB file should have non-zero size"
);
assert!(
db["node_count"].as_u64().unwrap_or(0) > 0,
"fixture indexed > 0 nodes"
);
assert!(db["journal_mode"].is_string() || db["journal_mode"].is_null());
}
#[tokio::test]
async fn test_health_detailed_includes_raw_signals() {
let (_dir, cg) = 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();
let dims = parsed.get("dimensions").expect("dimensions should exist");
for dim in [
"acyclicity",
"depth",
"equality",
"redundancy",
"modularity",
"coverage_discipline",
] {
let d = dims.get(dim).unwrap_or_else(|| panic!("missing {dim}"));
assert!(
d.get("score").is_some(),
"{dim}: 'score' field missing in details view"
);
assert!(
d.get("source").is_some(),
"{dim}: 'source' formula attribution missing"
);
}
assert!(dims["equality"].get("gini").is_some());
assert!(dims["equality"].get("interpretation").is_some());
assert!(dims["acyclicity"].get("edges_in_cycles").is_some());
assert!(dims["depth"].get("max_chain").is_some());
assert!(dims["depth"].get("ideal_chain").is_some());
assert!(dims["modularity"].get("interpretation").is_some());
assert!(dims["redundancy"].get("dead_count").is_some());
}
#[tokio::test]
async fn test_dsm_stats() {
let (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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_test_coverage_requires_exactly_one_input() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(&cg, "tokensave_test_coverage", json!({}), None, None).await;
let err = result.err().expect("should error with no input");
assert!(err.to_string().contains("exactly one"), "got: {err}");
let result = handle_tool_call(
&cg,
"tokensave_test_coverage",
json!({ "file": "src/lib.rs", "symbol": "foo" }),
None,
None,
)
.await;
let err = result.err().expect("should error with two inputs");
assert!(err.to_string().contains("exactly one"), "got: {err}");
}
#[tokio::test]
async fn test_test_coverage_file_mode_returns_rollup() {
let (_dir, cg) = setup_project().await;
let files = cg.get_all_files().await.unwrap();
let src = files
.iter()
.map(|fr| fr.path.clone())
.find(|p| p.ends_with(".rs") && !tokensave::tokensave::is_test_file(p))
.unwrap_or_else(|| "src/lib.rs".to_string());
let result = handle_tool_call(
&cg,
"tokensave_test_coverage",
json!({ "file": src }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["mode"], "file");
assert!(parsed["summary"].is_object());
assert!(parsed["summary"]["total_prod_fns"].is_u64());
assert!(parsed["summary"]["tested"].is_u64());
assert!(parsed["summary"]["untested"].is_u64());
assert!(parsed["tested"].is_array());
assert!(parsed["untested"].is_array());
}
#[tokio::test]
async fn source_path_override_is_used_by_coverage_and_risk_tools() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("components/test/__tests__")).unwrap();
fs::write(
project.join("components/test/widget.rs"),
"pub fn widget() -> bool { true }\n",
)
.unwrap();
fs::write(
project.join("components/test/__tests__/widget_test.rs"),
"#[test]\nfn widget_works() { assert!(true); }\n",
)
.unwrap();
let initial = TokenSave::init(project).await.unwrap();
drop(initial);
let mut config = load_config(project).unwrap();
config.source_path_overrides = vec!["components/test/**".to_string()];
save_config(project, &config).unwrap();
let cg = TokenSave::open(project).await.unwrap();
cg.index_all().await.unwrap();
let coverage = handle_tool_call(
&cg,
"tokensave_test_coverage",
json!({ "file": "components/test/widget.rs" }),
None,
None,
)
.await
.unwrap();
let coverage: Value = serde_json::from_str(extract_text(&coverage.value)).unwrap();
assert_eq!(coverage["summary"]["total_prod_fns"], 1);
assert_eq!(coverage["summary"]["test_only_fns"], 0);
let risk = handle_tool_call(
&cg,
"tokensave_test_risk",
json!({ "path": "components/test", "include_tested": true }),
None,
None,
)
.await
.unwrap();
let risk: Value = serde_json::from_str(extract_text(&risk.value)).unwrap();
assert_eq!(risk["summary"]["total_functions"], 1);
}
#[tokio::test]
async fn test_test_coverage_unknown_symbol_errors() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_test_coverage",
json!({ "symbol": "definitely_not_a_real_symbol_xyz" }),
None,
None,
)
.await;
let err = result.err().expect("should error on unknown symbol");
assert!(err.to_string().contains("not found"), "got: {err}");
}
#[tokio::test]
async fn test_test_coverage_clamps_max_depth() {
let (_dir, cg) = setup_project().await;
let files = cg.get_all_files().await.unwrap();
let src = files
.iter()
.map(|fr| fr.path.clone())
.find(|p| p.ends_with(".rs"))
.unwrap_or_else(|| "src/lib.rs".to_string());
let result = handle_tool_call(
&cg,
"tokensave_test_coverage",
json!({ "file": src, "max_depth": 100 }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["summary"]["max_depth"], 10);
}
fn write_test_cargo_toml(project: &std::path::Path) {
fs::write(
project.join("Cargo.toml"),
r#"[package]
name = "test_fixture"
version = "0.1.0"
[dependencies]
serde = "1.0"
[dev-dependencies]
tempfile = "3"
"#,
)
.unwrap();
}
#[tokio::test]
async fn test_dependencies_workspace_summary() {
let (dir, cg) = setup_project().await;
write_test_cargo_toml(dir.path());
let result = handle_tool_call(&cg, "tokensave_dependencies", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["mode"], "workspace");
assert!(parsed["members"].is_array());
let members: Vec<&str> = parsed["members"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(members.contains(&"test_fixture"));
let crates = parsed["crates"].as_array().unwrap();
assert!(crates.iter().any(|c| c["crate"] == "serde"));
}
#[tokio::test]
async fn test_dependencies_unknown_member_reports_available() {
let (dir, cg) = setup_project().await;
write_test_cargo_toml(dir.path());
let result = handle_tool_call(
&cg,
"tokensave_dependencies",
json!({ "member": "no_such_member_xyz" }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["mode"], "member");
assert!(parsed["error"]
.as_str()
.unwrap()
.contains("no_such_member_xyz"));
assert!(parsed["available_members"].is_array());
}
#[tokio::test]
async fn test_dependencies_kind_filter() {
let (dir, cg) = setup_project().await;
write_test_cargo_toml(dir.path());
let result = handle_tool_call(
&cg,
"tokensave_dependencies",
json!({ "kind": "dev" }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["kind_filter"], "dev");
let crates = parsed["crates"].as_array().unwrap();
let names: Vec<&str> = crates
.iter()
.map(|c| c["crate"].as_str().unwrap())
.collect();
assert!(names.contains(&"tempfile"), "dev dep should be present");
assert!(
!names.contains(&"serde"),
"normal dep should be filtered out"
);
}
#[tokio::test]
async fn test_dependencies_surfaces_license_and_drift() {
let (dir, cg) = setup_project().await;
fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("crates/alpha")).unwrap();
fs::create_dir_all(dir.path().join("crates/beta")).unwrap();
fs::write(
dir.path().join("crates/alpha/Cargo.toml"),
"[package]\nname = \"alpha\"\nversion = \"0.1.0\"\nlicense = \"MIT\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.unwrap();
fs::write(
dir.path().join("crates/beta/Cargo.toml"),
"[package]\nname = \"beta\"\nversion = \"0.1.0\"\nlicense = \"Apache-2.0\"\n\n[dependencies]\nserde = \"2.0\"\n",
)
.unwrap();
let result = handle_tool_call(&cg, "tokensave_dependencies", json!({}), None, None)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(extract_text(&result.value)).unwrap();
let licenses = parsed["licenses"].as_array().unwrap();
let names: Vec<&str> = licenses
.iter()
.map(|v| v["license"].as_str().unwrap())
.collect();
assert!(names.contains(&"MIT"));
assert!(names.contains(&"Apache-2.0"));
let detail = parsed["members_detail"].as_array().unwrap();
let alpha = detail.iter().find(|m| m["name"] == "alpha").unwrap();
assert_eq!(alpha["license"], "MIT");
let drift = parsed["version_drift"].as_array().unwrap();
let serde_drift = drift.iter().find(|d| d["crate"] == "serde").unwrap();
assert_eq!(serde_drift["version_count"], 2);
}
#[tokio::test]
async fn test_dependencies_include_lockfile_stamps_resolved() {
let (dir, cg) = setup_project().await;
fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nlicense = \"MIT\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.unwrap();
fs::write(
dir.path().join("Cargo.lock"),
"[[package]]\nname = \"serde\"\nversion = \"1.0.219\"\n",
)
.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_dependencies",
json!({ "crate": "serde", "include_lockfile": true }),
None,
None,
)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(extract_text(&result.value)).unwrap();
let usages = parsed["usages"].as_array().unwrap();
assert_eq!(usages[0]["version"], "1.0");
assert_eq!(usages[0]["resolved"], "1.0.219");
}
#[tokio::test]
async fn test_dependencies_crate_lookup() {
let (dir, cg) = setup_project().await;
write_test_cargo_toml(dir.path());
let result = handle_tool_call(
&cg,
"tokensave_dependencies",
json!({ "crate": "serde" }),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed["mode"], "crate");
assert_eq!(parsed["crate"], "serde");
let usages = parsed["usages"].as_array().unwrap();
assert_eq!(usages.len(), 1);
assert_eq!(usages[0]["member"], "test_fixture");
assert_eq!(usages[0]["version"], "1.0");
}
#[tokio::test]
async fn test_session_start() {
let (dir, cg) = setup_project().await;
for i in 0..10 {
cg.record_decision(&format!("decision {i}"), None, &[], &[])
.await
.unwrap();
}
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");
let decisions = output["memory_delta"]["recent_decisions"]
.as_array()
.expect("memory_delta.recent_decisions should be present");
assert!(!decisions.is_empty(), "delta should contain decisions");
assert!(
decisions.len() <= 5,
"delta decisions must be capped, got {}",
decisions.len()
);
assert!(decisions[0]["summary"].as_str().is_some());
}
#[tokio::test]
async fn test_session_end() {
let (dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 m = &output["matches"][0];
let body = m["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}"
);
assert!(
body.trim_end().ends_with('}'),
"body should end with the function's closing brace, got: {body:?}"
);
let start_line = m["start_line"].as_u64().unwrap() as usize;
let end_line = m["end_line"].as_u64().unwrap() as usize;
assert!(start_line >= 1, "start_line should be 1-based");
assert!(
end_line >= start_line,
"end_line should not precede start_line"
);
let file_rel = m["file"].as_str().unwrap();
let file_abs = _dir.path().join(file_rel);
let source = std::fs::read_to_string(&file_abs).unwrap();
let lines: Vec<&str> = source.lines().collect();
let end_line_text = lines
.get(end_line - 1)
.copied()
.unwrap_or_else(|| panic!("end_line {end_line} out of bounds in {file_rel}"));
assert!(
end_line_text.trim_end().ends_with('}'),
"end_line ({end_line}) should point at the closing brace; line text: {end_line_text:?}"
);
}
#[tokio::test]
async fn test_body_unknown_symbol() {
let (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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 (_dir, cg) = 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"));
}
#[tokio::test]
async fn test_handle_record_decision() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_record_decision",
json!({"text": "use JWT", "reason": "legal flagged sessions"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert!(
output.get("id").is_some(),
"response should contain 'id', got: {output}"
);
assert_eq!(
output["status"].as_str().unwrap(),
"recorded",
"status should be 'recorded', got: {output}"
);
}
#[tokio::test]
async fn test_handle_record_code_area() {
let (_dir, cg) = setup_project().await;
let result = handle_tool_call(
&cg,
"tokensave_record_code_area",
json!({"path": "src/auth.rs", "description": "OAuth provider"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
assert_eq!(
output["status"].as_str().unwrap(),
"recorded",
"status should be 'recorded', got: {output}"
);
}
#[tokio::test]
async fn test_handle_session_recall_returns_recorded_decision() {
let (_dir, cg) = setup_project().await;
handle_tool_call(
&cg,
"tokensave_record_decision",
json!({"text": "use JWT", "reason": "legal flagged sessions"}),
None,
None,
)
.await
.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_session_recall",
json!({"query": "JWT"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let decisions = output["decisions"]
.as_array()
.expect("decisions should be an array");
assert!(
!decisions.is_empty(),
"recall should return at least one decision after seeding"
);
let found = decisions
.iter()
.any(|d| d["text"].as_str().unwrap_or("").contains("JWT"));
assert!(
found,
"seeded 'JWT' decision should appear in recall results"
);
}
async fn setup_function_vs_field_collision() -> (TempDir, TokenSave) {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub struct Solvers {
pub gmres: u32,
}
pub fn gmres(x: u32) -> u32 {
x + 1
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
(dir, cg)
}
#[tokio::test]
async fn body_prefers_function_over_field_with_same_name() {
let (_dir, cg) = setup_function_vs_field_collision().await;
let result = handle_tool_call(
&cg,
"tokensave_body",
json!({"symbol": "gmres"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let matches = output["matches"].as_array().unwrap();
let first = &matches[0];
assert_eq!(
first["kind"].as_str(),
Some("function"),
"first match should be the function definition, got {first}"
);
let body = first["body"].as_str().unwrap();
assert!(
body.contains("pub fn gmres"),
"body should be the function source, got: {body}"
);
}
#[tokio::test]
async fn diff_context_dedupes_impacted_symbols() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
mod dep;
pub fn first() { dep::shared(); }
pub fn second() { dep::shared(); }
"#,
)
.unwrap();
fs::write(project.join("src/dep.rs"), "pub fn shared() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_diff_context",
json!({"files": ["src/lib.rs"], "depth": 3}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let impacted = output["impacted_symbols"].as_array().unwrap();
let mut ids: Vec<&str> = impacted.iter().filter_map(|v| v["id"].as_str()).collect();
ids.sort();
let before = ids.len();
ids.dedup();
let after = ids.len();
assert_eq!(
before, after,
"impacted_symbols must not contain duplicates by id; got {before} entries, {after} unique"
);
}
#[tokio::test]
async fn recursion_keeps_direct_recursion() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub fn recurse(n: u32) -> u32 {
if n == 0 { 0 } else { recurse(n - 1) }
}
pub fn nonrecursive() -> u32 { 42 }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_recursion", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
let has_recurse = cycles.iter().any(|cycle| {
cycle["chain"].as_array().is_some_and(|chain| {
chain
.iter()
.filter_map(|n| n["name"].as_str())
.filter(|name| *name == "recurse")
.count()
>= 2
})
});
assert!(
has_recurse,
"direct self-recursive function should be reported; got {cycles:?}"
);
}
#[tokio::test]
async fn recursion_filters_self_edge_artifacts() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub struct Triplet {
rows: Vec<usize>,
}
impl Triplet {
pub fn push(&mut self, row: usize) {
self.rows.push(row);
}
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_recursion", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
let mentions_push = cycles.iter().any(|cycle| {
cycle["chain"]
.as_array()
.is_some_and(|chain| chain.iter().any(|n| n["name"].as_str() == Some("push")))
});
assert!(
!mentions_push,
"`self.rows.push(...)` should not be reported as recursive; got {cycles:?}"
);
}
#[tokio::test]
async fn recursion_reports_real_cycle_path() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub fn a() { b(); }
pub fn b() { c(); }
pub fn c() { a(); }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_recursion", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
let chain = cycles
.iter()
.find_map(|cycle| {
let chain = cycle["chain"].as_array()?;
let names: Vec<&str> = chain.iter().filter_map(|n| n["name"].as_str()).collect();
(names.len() == 4).then_some(names)
})
.expect("expected a three-node cycle path");
let valid_edges = [("a", "b"), ("b", "c"), ("c", "a")];
for pair in chain.windows(2) {
assert!(
valid_edges.contains(&(pair[0], pair[1])),
"chain must follow real call edges; got {chain:?}"
);
}
}
#[tokio::test]
async fn changelog_filters_directory_paths() {
let dir = TempDir::new().unwrap();
let project = dir.path();
std::process::Command::new("git")
.args(["init"])
.current_dir(project)
.output()
.expect("git init");
std::process::Command::new("git")
.args(["config", "user.email", "t@t"])
.current_dir(project)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "t"])
.current_dir(project)
.output()
.unwrap();
fs::create_dir_all(project.join("src/sub")).unwrap();
fs::write(project.join("src/sub/keep.rs"), "pub fn k() {}\n").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(project)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(project)
.output()
.unwrap();
fs::write(
project.join("src/sub/keep.rs"),
"pub fn k() { let _ = 1; }\n",
)
.unwrap();
fs::write(project.join("src/sub/added.rs"), "pub fn a() {}\n").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(project)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "two"])
.current_dir(project)
.output()
.unwrap();
let cg = TokenSave::init(project).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);
let output: Value = serde_json::from_str(text).unwrap();
let changed: Vec<&str> = output["changed_files"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
for entry in &changed {
let p = project.join(entry);
assert!(
!p.is_dir(),
"changed_files must not include directories; got {entry:?}"
);
}
}
#[tokio::test]
async fn unused_imports_detects_truly_unused() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
use std::collections::HashMap;
use std::collections::HashSet;
mod inner;
pub fn used_one() -> HashMap<u32, u32> { HashMap::new() }
"#,
)
.unwrap();
fs::write(project.join("src/inner.rs"), "pub fn inner_fn() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_unused_imports", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let imports = output["imports"].as_array().unwrap();
let names: Vec<&str> = imports.iter().filter_map(|u| u["name"].as_str()).collect();
assert!(
names.iter().any(|n| n.contains("HashSet")),
"HashSet should be reported as unused; got names={names:?}"
);
}
#[tokio::test]
async fn dead_code_with_include_public_finds_pub_unreferenced() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub fn called() {}
pub fn never_called_anywhere() {}
pub fn caller() { called(); }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let default_result = handle_tool_call(&cg, "tokensave_dead_code", json!({}), None, None)
.await
.unwrap();
let default_text = extract_text(&default_result.value);
let default_output: Value = serde_json::from_str(default_text).unwrap();
assert_eq!(
default_output["dead_code_count"].as_u64().unwrap_or(99),
0,
"default dead_code (no include_public) must still skip pub items"
);
let with_pub = handle_tool_call(
&cg,
"tokensave_dead_code",
json!({"include_public": true}),
None,
None,
)
.await
.unwrap();
let with_pub_text = extract_text(&with_pub.value);
let with_pub_output: Value = serde_json::from_str(with_pub_text).unwrap();
let symbols: Vec<&str> = with_pub_output["symbols"]
.as_array()
.unwrap()
.iter()
.filter_map(|s| s["name"].as_str())
.collect();
assert!(
symbols.contains(&"never_called_anywhere"),
"with include_public, the pub unreferenced fn should appear; got {symbols:?}"
);
}
#[tokio::test]
async fn dependency_depth_excludes_implements_and_extends() {
use tokensave::graph::queries::GraphQueryManager;
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
mod a;
mod b;
"#,
)
.unwrap();
fs::write(
project.join("src/a.rs"),
r#"
#[derive(Debug, Clone)]
pub struct A;
"#,
)
.unwrap();
fs::write(
project.join("src/b.rs"),
r#"
pub trait T {}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let qm = GraphQueryManager::new(cg.db());
let adj = qm.build_file_adjacency(None).await.unwrap();
let from_a = adj.get("src/a.rs").cloned().unwrap_or_default();
let from_b = adj.get("src/b.rs").cloned().unwrap_or_default();
assert!(
!from_a.contains("src/b.rs"),
"src/a.rs must not depend on src/b.rs; got adj={from_a:?}"
);
assert!(
!from_b.contains("src/a.rs"),
"src/b.rs must not depend on src/a.rs; got adj={from_b:?}"
);
}
#[tokio::test]
async fn run_affected_tests_dispatches_directly_changed_test_files() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::create_dir_all(project.join("tests")).unwrap();
fs::write(project.join("src/lib.rs"), "pub fn util() -> u32 { 1 }\n").unwrap();
fs::write(
project.join("Cargo.toml"),
r#"[package]
name = "t"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
fs::write(
project.join("tests/edited_only.rs"),
r#"
#[test]
fn edited_only_test() {
assert_eq!(2, 2);
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_run_affected_tests",
json!({
"changed_paths": ["tests/edited_only.rs"],
"timeout_secs": 60,
"max_tests": 5
}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let dispatched = output["dispatched_tests"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect::<Vec<_>>()
})
.unwrap_or_default();
assert!(
dispatched.iter().any(|n| n.contains("edited_only_test")),
"expected edited_only_test to be dispatched; got dispatched={dispatched:?} note={:?}",
output["note"]
);
}
#[tokio::test]
async fn diagnose_normalizes_absolute_and_backslash_paths() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub fn target() {}\n").unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let abs_path = project.join("src/lib.rs");
let abs_str = abs_path.to_string_lossy().to_string();
let backslash_str = "src\\lib.rs";
let cargo_output = format!(
"error[E0001]: synthetic error\n --> {abs_str}:1:1\n |\n\nerror[E0002]: backslash form\n --> {backslash_str}:1:1\n |\n"
);
let result = handle_tool_call(
&cg,
"tokensave_diagnose",
json!({"cargo_output": cargo_output, "include_callers": false}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let mapped = output["mapped_to_node"].as_u64().unwrap_or(0);
assert_eq!(
mapped, 2,
"both diagnostics should map to nodes after path normalization; got mapped={mapped} full={output:#}"
);
}
#[tokio::test]
async fn resolver_blocklist_branch_respects_kind_filter() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub struct new;
pub fn caller() {
let _ = new();
helper();
}
pub fn helper() {}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let caller_id = find_node_id(&cg, "caller").await;
let result = handle_tool_call(
&cg,
"tokensave_callees",
json!({"node_id": caller_id, "max_depth": 1, "resolve_dispatch": false}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let items: Value = serde_json::from_str(text).unwrap();
let arr = items.as_array().unwrap();
for entry in arr {
let kind = entry["kind"].as_str().unwrap_or("");
let name = entry["name"].as_str().unwrap_or("");
let callable = matches!(
kind,
"function" | "method" | "struct_method" | "constructor" | "macro" | "arrow_function"
);
assert!(
callable,
"caller's callees must be callable kinds; got name={name} kind={kind} full={arr:#?}"
);
}
}
#[tokio::test]
async fn implements_refs_dont_resolve_to_enum_variants() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub enum Token { Default, Plus }
pub struct A;
impl Default for A { fn default() -> Self { A } }
pub struct B;
impl Default for B { fn default() -> Self { B } }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_rank",
json!({"edge_kind": "implements", "direction": "incoming"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let ranking = output["ranking"].as_array().unwrap();
for entry in ranking {
let kind = entry["kind"].as_str().unwrap_or("");
let name = entry["name"].as_str().unwrap_or("");
assert!(
kind != "enum_variant" && kind != "field",
"implements edges must not target {kind} (got name={name})"
);
}
}
#[tokio::test]
async fn circular_reports_one_entry_per_scc_not_per_walk() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "mod a; mod b; mod c;\n").unwrap();
fs::write(
project.join("src/a.rs"),
"use crate::b::b_fn;\npub fn a_fn() { b_fn(); }\n",
)
.unwrap();
fs::write(
project.join("src/b.rs"),
"use crate::c::c_fn;\npub fn b_fn() { c_fn(); }\n",
)
.unwrap();
fs::write(
project.join("src/c.rs"),
"use crate::a::a_fn;\npub fn c_fn() { a_fn(); }\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_circular", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycle_count = output["cycle_count"].as_u64().unwrap();
assert_eq!(
cycle_count, 1,
"three-file SCC must report exactly one cycle entry, got {cycle_count}"
);
let cycle = output["cycles"][0].as_array().unwrap();
assert_eq!(
cycle.len(),
3,
"the cycle should list all three files in the SCC; got {cycle:?}"
);
}
#[tokio::test]
async fn port_order_reports_separate_scc_groups() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub mod m;\n").unwrap();
fs::write(
project.join("src/m.rs"),
r#"
pub fn a() { b(); }
pub fn b() { a(); }
pub fn c() { d(); }
pub fn d() { c(); }
pub fn leaf() {}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_port_order",
json!({"source_dir": "src"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
assert!(
cycles.len() >= 2,
"expected at least 2 disjoint cycle groups; got {} entries: {cycles:?}",
cycles.len()
);
for c in cycles {
let names: Vec<&str> = c["symbols"]
.as_array()
.unwrap()
.iter()
.filter_map(|s| s["name"].as_str().or_else(|| s.as_str()))
.collect();
let has_ab = names.iter().any(|n| *n == "a" || *n == "b");
let has_cd = names.iter().any(|n| *n == "c" || *n == "d");
assert!(
!(has_ab && has_cd),
"one cycle entry contains both SCCs (a/b mixed with c/d): {names:?}"
);
}
}
#[tokio::test]
async fn port_order_provides_intra_cycle_ordering() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub mod m;\n").unwrap();
fs::write(
project.join("src/m.rs"),
r#"
pub fn a() { b(); h(); }
pub fn b() { c(); h(); }
pub fn c() { a(); h(); }
pub fn h() { a(); }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_port_order",
json!({"source_dir": "src"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
assert!(!cycles.is_empty(), "expected at least one cycle");
let cycle = &cycles[0];
assert!(
cycle["files"].as_array().is_some(),
"cycle must carry a `files` breakdown"
);
let files_arr = cycle["files"].as_array().unwrap();
for f in files_arr {
assert!(
f.is_object() && f["members_in_cycle"].as_u64().is_some(),
"files entries must be objects with `members_in_cycle`, got {f}"
);
}
let symbols = cycle["symbols"].as_array().unwrap();
for s in symbols {
assert!(
s["in_cycle_out_degree"].as_u64().is_some(),
"each symbol must report in_cycle_out_degree; got {s}"
);
assert!(
s["in_cycle_in_degree"].as_u64().is_some(),
"each symbol must report in_cycle_in_degree; got {s}"
);
}
assert!(
cycle["entry_point"].is_object(),
"cycle must surface a suggested entry_point; got {cycle}"
);
assert!(
cycle["break_point_candidate"].is_object(),
"cycle must surface a break_point_candidate; got {cycle}"
);
assert_eq!(
cycle["break_point_candidate"]["name"].as_str(),
Some("h"),
"break_point_candidate should be the hub function `h`; got {cycle}"
);
}
#[tokio::test]
async fn port_order_ignores_self_edges() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub mod m;\n").unwrap();
fs::write(
project.join("src/m.rs"),
r#"
pub struct Triplet {
rows: Vec<usize>,
}
impl Triplet {
pub fn push(&mut self, row: usize) {
self.rows.push(row);
}
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_port_order",
json!({"source_dir": "src"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
assert!(
cycles.is_empty(),
"self-edge-only methods should stay out of port_order cycles: {cycles:?}"
);
}
#[tokio::test]
async fn inheritance_depth_walks_rust_supertraits() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub trait Base {}
pub trait Middle: Base {}
pub trait Leaf: Middle {}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_inheritance_depth", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let ranking = output["ranking"].as_array().unwrap();
let names: Vec<&str> = ranking.iter().filter_map(|r| r["name"].as_str()).collect();
assert!(
names.contains(&"Leaf"),
"expected Leaf trait in inheritance_depth ranking; got {names:?}"
);
let leaf = ranking
.iter()
.find(|r| r["name"].as_str() == Some("Leaf"))
.unwrap();
let depth = leaf["depth"].as_u64().unwrap();
assert!(depth >= 2, "Leaf depth should be >= 2 hops, got {depth}");
}
#[tokio::test]
async fn circular_emits_disjoint_sccs_under_load() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
let mut lib_rs = String::new();
for k in 0..5 {
lib_rs.push_str(&format!("pub mod a{k};\npub mod b{k};\npub mod c{k};\n"));
}
fs::write(project.join("src/lib.rs"), lib_rs).unwrap();
for k in 0..5 {
let next = (k + 1) % 5;
fs::write(
project.join(format!("src/a{k}.rs")),
format!("use crate::b{k}::b_fn;\npub fn a_fn() {{ b_fn(); }}\n"),
)
.unwrap();
fs::write(
project.join(format!("src/b{k}.rs")),
format!("use crate::c{k}::c_fn;\npub fn b_fn() {{ c_fn(); }}\n"),
)
.unwrap();
fs::write(
project.join(format!("src/c{k}.rs")),
format!(
"use crate::a{k}::a_fn;\nuse crate::a{next}::a_fn as next_a;\npub fn c_fn() {{ a_fn(); next_a(); }}\n"
),
)
.unwrap();
}
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_circular", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let cycles = output["cycles"].as_array().unwrap();
use std::collections::HashSet;
let mut seen: HashSet<String> = HashSet::new();
for cycle in cycles {
let files = cycle.as_array().unwrap();
for f in files {
let s = f.as_str().unwrap().to_string();
assert!(
seen.insert(s.clone()),
"file {s} appears in more than one cycle entry; SCCs must be disjoint"
);
}
}
}
#[tokio::test]
async fn diff_context_dedupes_modified_symbols_on_duplicate_input() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
"pub struct S; pub fn one() {} pub fn two() {}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_diff_context",
json!({"files": ["src/lib.rs", "src/lib.rs", "src/lib.rs"], "depth": 1}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let modified = output["modified_symbols"].as_array().unwrap();
let mut ids: Vec<&str> = modified.iter().filter_map(|v| v["id"].as_str()).collect();
let before = ids.len();
ids.sort();
ids.dedup();
let after = ids.len();
assert_eq!(
before, after,
"modified_symbols must not contain duplicate ids even when input has the same file 3×; got {before} entries, {after} unique"
);
}
#[tokio::test]
async fn changelog_filters_deleted_directory_entries() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fn git(cwd: &std::path::Path, args: &[&str]) {
std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.unwrap_or_else(|_| panic!("git {args:?} failed"));
}
git(project, &["init"]);
git(project, &["config", "user.email", "t@t"]);
git(project, &["config", "user.name", "t"]);
fs::create_dir_all(project.join("crates/sub")).unwrap();
fs::write(project.join("crates/sub/keep.rs"), "pub fn k() {}\n").unwrap();
fs::write(project.join("main.rs"), "fn main() {}\n").unwrap();
git(project, &["add", "."]);
git(project, &["commit", "-m", "init"]);
fs::remove_dir_all(project.join("crates")).unwrap();
git(project, &["add", "-A"]);
git(project, &["commit", "-m", "drop crates"]);
let cg = TokenSave::init(project).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);
let output: Value = serde_json::from_str(text).unwrap();
let changed: Vec<String> = output["changed_files"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let problematic: Vec<&String> = changed.iter().filter(|p| !p.ends_with(".rs")).collect();
assert!(
problematic.is_empty(),
"changed_files should be file paths only (no directories like 'crates' or 'crates/sub'); got problematic={problematic:?} full={changed:?}"
);
}
#[tokio::test]
async fn pr_context_collapses_cargo_toml_keys() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fn git(cwd: &std::path::Path, args: &[&str]) {
std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.unwrap_or_else(|_| panic!("git {args:?} failed"));
}
git(project, &["init"]);
git(project, &["config", "user.email", "t@t"]);
git(project, &["config", "user.name", "t"]);
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"x\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
)
.unwrap();
fs::write(project.join("src/lib.rs"), "pub fn a() {}\n").unwrap();
git(project, &["add", "."]);
git(project, &["commit", "-m", "init"]);
let mut bloated = String::from(
"[package]\nname = \"x\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
);
for i in 0..50 {
bloated.push_str(&format!("dep{i} = \"0.1.{i}\"\n"));
}
fs::write(project.join("Cargo.toml"), &bloated).unwrap();
git(project, &["add", "."]);
git(project, &["commit", "-m", "deps"]);
let cg = TokenSave::init(project).await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_pr_context",
json!({"base_ref": "HEAD~1", "head_ref": "HEAD"}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let added = output["added"].as_array().unwrap();
let modified = output["modified"].as_array().unwrap();
let count_cargo = |arr: &[Value]| -> usize {
arr.iter()
.filter(|v| v["file"].as_str() == Some("Cargo.toml"))
.count()
};
let cargo_total = count_cargo(added) + count_cargo(modified);
assert!(
cargo_total <= 1,
"Cargo.toml should collapse to at most one summary symbol; got {cargo_total} entries. added={added:?}, modified={modified:?}"
);
let summary = modified
.iter()
.find(|v| v["file"].as_str() == Some("Cargo.toml"));
assert!(
summary.is_some(),
"expected one config_summary entry for Cargo.toml in modified; got {modified:?}"
);
assert_eq!(
summary.unwrap()["kind"].as_str(),
Some("config_summary"),
"Cargo.toml entry should be kind=config_summary"
);
}
#[tokio::test]
async fn pr_context_uses_branch_meta_default_branch() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fn git(cwd: &std::path::Path, args: &[&str]) {
std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.unwrap_or_else(|_| panic!("git {args:?} failed"));
}
git(project, &["init", "-b", "master"]);
git(project, &["config", "user.email", "t@t"]);
git(project, &["config", "user.name", "t"]);
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/lib.rs"), "pub fn a() {}\n").unwrap();
git(project, &["add", "."]);
git(project, &["commit", "-m", "init"]);
git(project, &["checkout", "-b", "feature"]);
fs::write(project.join("src/lib.rs"), "pub fn a() {}\npub fn b() {}\n").unwrap();
git(project, &["add", "."]);
git(project, &["commit", "-m", "add b"]);
let cg = TokenSave::init(project).await.unwrap();
let tokensave_dir = tokensave::config::get_tokensave_dir(cg.project_root());
let meta = tokensave::branch_meta::BranchMeta::new("master");
tokensave::branch_meta::save_branch_meta(&tokensave_dir, &meta).unwrap();
let result = handle_tool_call(&cg, "tokensave_pr_context", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("git error"),
"pr_context should resolve default branch from branch-meta, got: {text}"
);
let output: Value = serde_json::from_str(text).unwrap();
assert!(
output.get("added").is_some() || output.get("modified").is_some(),
"expected pr_context to return diff data, got: {text}"
);
}
#[tokio::test]
async fn unused_imports_handles_grouped_use() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
use std::collections::{HashMap, HashSet};
pub fn used() -> HashMap<u32, u32> { HashMap::new() }
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_unused_imports", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let imports = output["imports"].as_array().unwrap();
let payloads: Vec<String> = imports
.iter()
.map(|u| {
format!(
"{}::{}",
u["name"].as_str().unwrap_or(""),
u["unused"].as_str().unwrap_or("")
)
})
.collect();
let mentions_hashset = imports.iter().any(|u| {
u["unused"].as_str().is_some_and(|s| s.contains("HashSet"))
|| u["name"].as_str().is_some_and(|n| n.contains("HashSet"))
});
assert!(
mentions_hashset,
"HashSet from grouped use should be reported as unused; got {payloads:?}"
);
let any_falsely_flags_hashmap = imports
.iter()
.any(|u| u["unused"].as_str().is_some_and(|s| s == "HashMap"));
assert!(
!any_falsely_flags_hashmap,
"HashMap is used (HashMap::new()) and must not appear in `unused`; got {payloads:?}"
);
}
#[tokio::test]
async fn dead_code_flags_unreferenced_fn_with_attribute() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
fn caller() {
used_helper();
}
#[inline]
fn used_helper() {}
#[inline]
fn dead_helper_with_attr() {}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(&cg, "tokensave_dead_code", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let output: Value = serde_json::from_str(text).unwrap();
let symbols = output["symbols"].as_array().unwrap();
let names: Vec<&str> = symbols.iter().filter_map(|s| s["name"].as_str()).collect();
assert!(
names.contains(&"dead_helper_with_attr"),
"private fn with #[inline] and no callers should be dead; got {names:?}"
);
assert!(
!names.contains(&"used_helper"),
"used_helper has a real caller and must NOT appear; got {names:?}"
);
}
#[tokio::test]
async fn search_ranks_trait_definition_above_use_reexports() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src/a")).unwrap();
fs::create_dir_all(project.join("src/b")).unwrap();
fs::create_dir_all(project.join("src/c")).unwrap();
fs::create_dir_all(project.join("src/d")).unwrap();
fs::create_dir_all(project.join("src/e")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub mod operator;
pub mod a;
pub mod b;
pub mod c;
pub mod d;
pub mod e;
"#,
)
.unwrap();
fs::write(
project.join("src/operator.rs"),
"pub trait LinearOperator { fn apply(&self); }\n",
)
.unwrap();
for sub in ["a", "b", "c", "d", "e"] {
fs::write(
project.join(format!("src/{sub}/mod.rs")),
"pub use crate::operator::LinearOperator;\n",
)
.unwrap();
}
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "LinearOperator", "limit": 10}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let items: Value = serde_json::from_str(text).unwrap();
let arr = items.as_array().unwrap();
let first_kind = arr[0]["kind"].as_str().unwrap_or("");
assert_eq!(
first_kind, "trait",
"first search hit for LinearOperator should be the trait definition, got '{first_kind}' (full: {arr:?})"
);
}
#[tokio::test]
async fn search_doc_penalty_on_additive_query_path() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::create_dir_all(project.join("docs")).unwrap();
fs::write(project.join("src/lib.rs"), "pub mod config;\n").unwrap();
fs::write(
project.join("src/config.rs"),
"pub fn parse_configuration() {}\n",
)
.unwrap();
fs::write(
project.join("README.md"),
"# Configuration\n\nHow to configure the thing.\n",
)
.unwrap();
fs::write(
project.join("docs/guide.md"),
"# Configuration Guide\n\nLonger prose.\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "Configuration", "limit": 10}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
let items: Value = serde_json::from_str(text).unwrap();
let arr = items.as_array().unwrap();
let first_kind = arr[0]["kind"].as_str().unwrap_or("");
assert_eq!(
first_kind, "function",
"code definition must rank above the verbatim-matching doc heading, got {arr:?}"
);
let doc_names: Vec<&str> = arr
.iter()
.filter(|r| r["file"].as_str().is_some_and(|f| f.ends_with(".md")))
.filter_map(|r| r["name"].as_str())
.collect();
assert_eq!(
doc_names,
vec!["Configuration", "Configuration Guide"],
"headings must stay reachable, exact-named heading first, got {arr:?}"
);
}
#[tokio::test]
async fn refresh_file_token_map_picks_up_new_files() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path();
std::fs::write(project.join("a.rs"), "fn a() {}").unwrap();
let cg = tokensave::tokensave::TokenSave::init(project)
.await
.unwrap();
cg.sync().await.unwrap();
let server = tokensave::mcp::McpServer::new(cg, None).await;
let initial_map = server.file_token_map_snapshot();
let initial_keys: std::collections::HashSet<_> = initial_map.keys().cloned().collect();
std::fs::write(project.join("b.rs"), "fn b() { let y = 2; }").unwrap();
let cg2 = tokensave::tokensave::TokenSave::open(project)
.await
.unwrap();
cg2.sync().await.unwrap();
server.refresh_file_token_map().await;
let after_map = server.file_token_map_snapshot();
let after_keys: std::collections::HashSet<_> = after_map.keys().cloned().collect();
assert!(
after_keys.len() > initial_keys.len(),
"refresh should pick up b.rs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_server_owns_watcher_and_refreshes_token_map_on_change() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path();
std::fs::write(project.join("a.rs"), "fn a() {}").unwrap();
let cg = tokensave::tokensave::TokenSave::init(project)
.await
.unwrap();
cg.sync().await.unwrap();
let server = tokensave::mcp::McpServer::new(cg, None).await;
let initial_count = server.file_token_map_snapshot().len();
std::fs::write(project.join("b.rs"), "fn b() {}").unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
let after_count = loop {
let stale = server.cg().find_stale_files().await;
server.cg().sync_if_stale_silent(&stale).await.unwrap();
server.refresh_file_token_map().await;
let count = server.file_token_map_snapshot().len();
if count > initial_count || std::time::Instant::now() >= deadline {
break count;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
};
assert!(
after_count > initial_count,
"lazy sync should have refreshed map ({initial_count} -> {after_count})"
);
server.shutdown().await;
}
async fn setup_vendored_project() -> (TempDir, TokenSave) {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::create_dir_all(project.join("third_party")).unwrap();
fs::write(
project.join("src/app.rs"),
r#"
/// App-level helper.
pub fn helper() -> String {
String::from("app")
}
"#,
)
.unwrap();
fs::write(
project.join("third_party/lib.rs"),
r#"
/// Vendored helper.
pub fn helper() -> String {
String::from("vendor")
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
(dir, cg)
}
#[tokio::test]
async fn test_search_path_exclude_drops_vendored() {
let (_dir, cg) = setup_vendored_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper", "limit": 20, "path_exclude": ["third_party/"]}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("third_party/lib.rs"),
"path_exclude should drop vendored results, got: {text}"
);
assert!(
text.contains("src/app.rs"),
"path_exclude should keep app results, got: {text}"
);
}
#[tokio::test]
async fn test_search_path_include_keeps_only_matching() {
let (_dir, cg) = setup_vendored_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper", "limit": 20, "path_include": ["third_party/"]}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("third_party/lib.rs"),
"path_include should keep vendored results, got: {text}"
);
assert!(
!text.contains("src/app.rs"),
"path_include should drop non-matching results, got: {text}"
);
}
#[tokio::test]
async fn test_search_no_path_filter_unchanged() {
let (_dir, cg) = setup_vendored_project().await;
let result = handle_tool_call(
&cg,
"tokensave_search",
json!({"query": "helper", "limit": 20}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
text.contains("third_party/lib.rs") && text.contains("src/app.rs"),
"no path filter should return both trees, got: {text}"
);
}
#[tokio::test]
async fn test_context_path_exclude_drops_vendored() {
let (_dir, cg) = setup_vendored_project().await;
let result = handle_tool_call(
&cg,
"tokensave_context",
json!({"task": "understand the helper functions", "path_exclude": ["third_party/"]}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("third_party/lib.rs"),
"context path_exclude should drop vendored entry points, got: {text}"
);
}
#[tokio::test]
async fn test_context_path_include_keeps_only_matching() {
let (_dir, cg) = setup_vendored_project().await;
let result = handle_tool_call(
&cg,
"tokensave_context",
json!({"task": "understand the helper functions", "path_include": ["third_party/"]}),
None,
None,
)
.await
.unwrap();
let text = extract_text(&result.value);
assert!(
!text.contains("src/app.rs"),
"context path_include should drop non-matching entry points, got: {text}"
);
}
#[tokio::test]
async fn callers_resolve_generic_trait_calls_for_concrete_impl_method() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/lib.rs"),
r#"
pub trait LinearOperator {
fn apply_into(&self, input: &[f64], output: &mut [f64]);
}
pub struct Matrix;
impl LinearOperator for Matrix {
fn apply_into(&self, input: &[f64], output: &mut [f64]) {
output.copy_from_slice(input);
}
}
pub fn residual_vector<T: LinearOperator>(operator: &T, x: &[f64], ax: &mut [f64]) {
let _ = (operator, x, ax);
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let mut concrete_id = None;
for method in cg.get_nodes_by_name("apply_into").await.unwrap() {
let Some(parent_id) = method.parent_id.as_deref() else {
continue;
};
if cg
.get_node(parent_id)
.await
.unwrap()
.is_some_and(|parent| parent.kind == tokensave::types::NodeKind::Impl)
{
concrete_id = Some(method.id);
break;
}
}
let concrete_id = concrete_id.expect("concrete Matrix::apply_into method");
let concrete = cg.get_node(&concrete_id).await.unwrap().unwrap();
let sources = cg.get_trait_dispatch_sources(&concrete).await.unwrap();
assert!(
!sources.is_empty(),
"concrete method should resolve back to its trait method; concrete={concrete:#?} parent={:#?}",
cg.get_node(concrete.parent_id.as_deref().unwrap())
.await
.unwrap()
);
let residual_id = find_node_id(&cg, "residual_vector").await;
cg.db()
.insert_edges(&[tokensave::types::Edge {
source: residual_id,
target: sources[0].id.clone(),
kind: tokensave::types::EdgeKind::Calls,
line: Some(14),
}])
.await
.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_callers",
json!({"node_id": concrete_id, "max_depth": 1}),
None,
None,
)
.await
.unwrap();
let callers: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
let residual = callers
.as_array()
.unwrap()
.iter()
.find(|caller| caller["name"] == "residual_vector")
.expect("generic caller should be connected to concrete impl method");
assert_eq!(residual["dispatch_via_trait"], true);
}
#[tokio::test]
async fn affected_promotes_directly_changed_inline_test_source() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("Cargo.toml"),
"[package]\nname='inline-tests'\nversion='0.1.0'\nedition='2021'\n",
)
.unwrap();
fs::write(
project.join("src/lib.rs"),
r#"pub fn changed_api() {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_inline_check() {
changed_api();
}
#[test]
fn second_inline_check() {
changed_api();
}
}
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_affected",
json!({"files": ["src/lib.rs"], "depth": 5}),
None,
None,
)
.await
.unwrap();
let output: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(output["affected_tests"], json!(["src/lib.rs"]));
assert_eq!(output["recommended_tests"], json!(["src/lib.rs"]));
assert_eq!(
output["inline_test_sources"],
json!([{"file": "src/lib.rs", "distance": 0}])
);
assert_eq!(output["count"], 1);
let candidate = output["classified_candidates"]
.as_array()
.unwrap()
.iter()
.find(|item| item["file"] == "src/lib.rs")
.unwrap();
assert_eq!(candidate["category"], "inline_test_source");
assert_eq!(candidate["distance"], 0);
assert_eq!(candidate["confidence"], "high");
}
#[tokio::test]
async fn affected_classifies_candidates_and_includes_inline_sources_in_suite() {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::create_dir_all(project.join("tests")).unwrap();
fs::create_dir_all(project.join("crates/consumer/src")).unwrap();
fs::create_dir_all(project.join("crates/consumer/tests")).unwrap();
fs::write(
project.join("Cargo.toml"),
"[package]\nname='root'\nversion='0.1.0'\nedition='2021'\n",
)
.unwrap();
fs::write(
project.join("crates/consumer/Cargo.toml"),
"[package]\nname='consumer'\nversion='0.1.0'\nedition='2021'\n",
)
.unwrap();
fs::write(project.join("src/core.rs"), "pub fn changed_api() {}\n").unwrap();
fs::write(
project.join("src/inline.rs"),
"#[test]\nfn inline_check() {}\n",
)
.unwrap();
fs::write(project.join("src/facade.rs"), "pub fn facade_call() {}\n").unwrap();
fs::write(
project.join("tests/core_test.rs"),
"#[test]\nfn direct_check() {}\n",
)
.unwrap();
fs::write(
project.join("tests/integration_test.rs"),
"#[test]\nfn integration_check() {}\n",
)
.unwrap();
fs::write(
project.join("crates/consumer/src/lib.rs"),
"pub fn consumer_bridge() {}\n",
)
.unwrap();
fs::write(
project.join("crates/consumer/tests/use_core.rs"),
"#[test]\nfn consumer_check() {}\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let changed = find_node_id(&cg, "changed_api").await;
let inline = find_node_id(&cg, "inline_check").await;
let direct = find_node_id(&cg, "direct_check").await;
let facade = find_node_id(&cg, "facade_call").await;
let integration = find_node_id(&cg, "integration_check").await;
let bridge = find_node_id(&cg, "consumer_bridge").await;
let consumer = find_node_id(&cg, "consumer_check").await;
cg.db()
.insert_edges(&[
tokensave::types::Edge {
source: inline,
target: changed.clone(),
kind: tokensave::types::EdgeKind::Calls,
line: Some(1),
},
tokensave::types::Edge {
source: direct,
target: changed.clone(),
kind: tokensave::types::EdgeKind::Calls,
line: Some(1),
},
tokensave::types::Edge {
source: facade.clone(),
target: changed.clone(),
kind: tokensave::types::EdgeKind::Calls,
line: Some(1),
},
tokensave::types::Edge {
source: integration,
target: facade,
kind: tokensave::types::EdgeKind::Calls,
line: Some(3),
},
tokensave::types::Edge {
source: bridge.clone(),
target: changed,
kind: tokensave::types::EdgeKind::Calls,
line: Some(1),
},
tokensave::types::Edge {
source: consumer,
target: bridge,
kind: tokensave::types::EdgeKind::Calls,
line: Some(1),
},
])
.await
.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_affected",
json!({"files": ["src/core.rs"], "depth": 5}),
None,
None,
)
.await
.unwrap();
let output: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
let affected = output["affected_tests"].as_array().unwrap();
assert!(affected.iter().any(|file| file == "src/inline.rs"));
assert!(output["inline_test_sources"]
.as_array()
.unwrap()
.iter()
.any(|item| item["file"] == "src/inline.rs"));
assert!(output["classified_candidates"]
.as_array()
.unwrap()
.iter()
.any(|item| item["file"] == "src/inline.rs"
&& item["category"] == "inline_test_source"
&& item["confidence"] == "high"));
assert!(output["classified_candidates"]
.as_array()
.unwrap()
.iter()
.any(|item| item["category"] == "same_crate_integration"));
assert!(output["classified_candidates"]
.as_array()
.unwrap()
.iter()
.any(|item| item["category"] == "cross_crate_consumer"));
assert!(output["recommended_tests"]
.as_array()
.unwrap()
.iter()
.any(|file| file == "src/inline.rs"));
assert!(!output["recommended_tests"]
.as_array()
.unwrap()
.iter()
.any(|file| file == "crates/consumer/tests/use_core.rs"));
}
#[tokio::test]
async fn test_str_replace_resolved_path_for_relative_path_in_root() {
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 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["file_path"], "src/main.rs");
let expected_resolved = project.join("src/main.rs").to_string_lossy().to_string();
assert_eq!(parsed["resolved_path"], expected_resolved);
}
#[tokio::test]
async fn test_str_replace_absolute_path_outside_root_honored_verbatim() {
let project_dir = TempDir::new().unwrap();
let project = 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 outside_dir = TempDir::new().unwrap();
let outside_file = outside_dir.path().join("notes.txt");
fs::write(&outside_file, "todo: fix the bug\n").unwrap();
let result = handle_tool_call(
&cg,
"tokensave_str_replace",
json!({
"path": outside_file.to_string_lossy(),
"old_str": "todo: fix the bug",
"new_str": "done: fixed the bug"
}),
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,
"absolute path outside the indexed root must be honored, not rejected: {text}"
);
let expected = outside_file.to_string_lossy().to_string();
assert_eq!(parsed["resolved_path"], expected);
let content = fs::read_to_string(&outside_file).unwrap();
assert_eq!(content, "done: fixed the bug\n");
let project_content = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert_eq!(project_content, "fn hello() {}\n");
}
#[tokio::test]
async fn test_str_replace_project_root_override_writes_to_worktree_not_primary_checkout() {
let primary = TempDir::new().unwrap();
let primary_root = primary.path();
fs::create_dir_all(primary_root.join("src")).unwrap();
fs::write(
primary_root.join("src/main.rs"),
"fn hello() { \"primary\" }\n",
)
.unwrap();
let cg = TokenSave::init(primary_root).await.unwrap();
cg.index_all().await.unwrap();
let worktree = TempDir::new().unwrap();
let worktree_root = worktree.path();
fs::create_dir_all(worktree_root.join("src")).unwrap();
fs::write(
worktree_root.join("src/main.rs"),
"fn hello() { \"worktree\" }\n",
)
.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_str_replace",
json!({
"path": "src/main.rs",
"old_str": "fn hello() { \"worktree\" }",
"new_str": "fn hello() { \"worktree-updated\" }",
"project_root": worktree_root.to_string_lossy()
}),
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, "got: {text}");
let expected_resolved = worktree_root
.join("src/main.rs")
.to_string_lossy()
.to_string();
assert_eq!(parsed["resolved_path"], expected_resolved);
let worktree_content = fs::read_to_string(worktree_root.join("src/main.rs")).unwrap();
assert_eq!(worktree_content, "fn hello() { \"worktree-updated\" }\n");
let primary_content = fs::read_to_string(primary_root.join("src/main.rs")).unwrap();
assert_eq!(primary_content, "fn hello() { \"primary\" }\n");
}
#[tokio::test]
async fn test_insert_at_absolute_path_outside_root_honored_verbatim() {
let project_dir = TempDir::new().unwrap();
let project = 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 outside_dir = TempDir::new().unwrap();
let outside_file = outside_dir.path().join("scratch.txt");
fs::write(&outside_file, "line one\nline two\n").unwrap();
let result = handle_tool_call(
&cg,
"tokensave_insert_at",
json!({
"path": outside_file.to_string_lossy(),
"anchor": "line one",
"content": "inserted line",
"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, "got: {text}");
let expected = outside_file.to_string_lossy().to_string();
assert_eq!(parsed["resolved_path"], expected);
let content = fs::read_to_string(&outside_file).unwrap();
assert_eq!(content, "line one\ninserted line\nline two\n");
}
#[tokio::test]
async fn test_replace_symbol_project_root_override_writes_to_worktree_not_primary_checkout() {
let primary = TempDir::new().unwrap();
let primary_root = primary.path();
fs::create_dir_all(primary_root.join("src")).unwrap();
let original_source = "pub fn greet_widget_example() -> String {\n \"hi\".to_string()\n}\n";
fs::write(primary_root.join("src/lib.rs"), original_source).unwrap();
let cg = TokenSave::init(primary_root).await.unwrap();
cg.index_all().await.unwrap();
let worktree = TempDir::new().unwrap();
let worktree_root = worktree.path();
fs::create_dir_all(worktree_root.join("src")).unwrap();
fs::write(worktree_root.join("src/lib.rs"), original_source).unwrap();
let new_source =
"pub fn greet_widget_example() -> String {\n \"hi from worktree\".to_string()\n}";
let result = handle_tool_call(
&cg,
"tokensave_replace_symbol",
json!({
"symbol": "greet_widget_example",
"new_source": new_source,
"project_root": worktree_root.to_string_lossy()
}),
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, "got: {text}");
let expected_resolved = worktree_root
.join("src/lib.rs")
.to_string_lossy()
.to_string();
assert_eq!(parsed["resolved_path"], expected_resolved);
let worktree_content = fs::read_to_string(worktree_root.join("src/lib.rs")).unwrap();
assert!(worktree_content.contains("hi from worktree"));
let primary_content = fs::read_to_string(primary_root.join("src/lib.rs")).unwrap();
assert_eq!(primary_content, original_source);
}
fn run_git(dir: &std::path::Path, args: &[&str]) {
let out = std::process::Command::new("git")
.args(args)
.current_dir(dir)
.output()
.expect("failed to run git");
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
fn init_git_with_commits(project: &std::path::Path) {
run_git(project, &["init"]);
run_git(project, &["config", "user.email", "test@test.com"]);
run_git(project, &["config", "user.name", "Test"]);
run_git(project, &["config", "commit.gpgsign", "false"]);
run_git(project, &["add", "."]);
run_git(project, &["commit", "-m", "initial"]);
fs::write(project.join("src/extra.rs"), "pub fn extra() {}\n").unwrap();
run_git(project, &["add", "."]);
run_git(project, &["commit", "-m", "second"]);
}
#[tokio::test]
async fn test_status_mid_rebuild_reports_rebuild_in_progress() {
let (dir, cg) = setup_project().await;
let project = dir.path();
init_git_with_commits(project);
cg.db().clear().await.unwrap();
let _lock = tokensave::tokensave::try_acquire_sync_lock(project).unwrap();
let result = handle_tool_call(&cg, "tokensave_status", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let status: Value = serde_json::from_str(text).expect("status must be JSON");
assert_eq!(status["node_count"], json!(0));
assert_eq!(
status["index_rebuild_in_progress"],
json!(true),
"status must flag an in-flight rebuild instead of silently presenting \
an empty graph as the true index state (#267), got: {text}"
);
assert!(
status.get("stale_warning").is_none(),
"an in-flight rebuild must not produce a bogus whole-history \
staleness warning (#267), got: {text}"
);
}
#[tokio::test]
async fn test_status_staleness_falls_back_to_last_sync_at_when_files_empty() {
let (dir, cg) = setup_project().await;
let project = dir.path();
init_git_with_commits(project);
cg.db().clear().await.unwrap();
let future = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600;
cg.db()
.set_metadata("last_sync_at", &future.to_string())
.await
.unwrap();
let result = handle_tool_call(&cg, "tokensave_status", json!({}), None, None)
.await
.unwrap();
let text = extract_text(&result.value);
let status: Value = serde_json::from_str(text).expect("status must be JSON");
assert!(
status.get("stale_commits").is_none(),
"stale_commits must not count the entire branch history when the \
files table is empty (#267), got: {text}"
);
assert_eq!(
status["version"],
json!(env!("CARGO_PKG_VERSION")),
"status must report the running tokensave version, got: {text}"
);
}
async fn setup_documented_project() -> (TempDir, TokenSave) {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::create_dir_all(project.join("tokensave-docs")).unwrap();
fs::write(project.join("src/big_class.rs"), "pub fn parse_it() {}\n").unwrap();
fs::write(
project.join("src/big_class.readme.md"),
"# Big Class\n\nOrchestrates parsing; read this before the 3000-line body.\n",
)
.unwrap();
fs::write(project.join("src/search_es8.rs"), "pub fn query_es8() {}\n").unwrap();
fs::write(project.join("src/feed_es8.rs"), "pub fn feed_es8() {}\n").unwrap();
fs::write(project.join("src/plain.rs"), "pub fn plain() {}\n").unwrap();
fs::write(
project.join("tokensave-docs/es8.md"),
"---\napplies_to:\n - \"**/*_es8.rs\"\n---\n\n# ES8 driver\n\nUse the raw driver, not the standard syntax.\n",
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
(dir, cg)
}
#[tokio::test]
async fn doc_tool_returns_sidecar_documentation() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_doc",
json!({"file": "src/big_class.rs"}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], true, "{parsed:?}");
assert_eq!(parsed["count"], 1, "{parsed:?}");
let doc = &parsed["docs"][0];
assert_eq!(doc["doc_path"], "src/big_class.readme.md");
assert_eq!(doc["covers"], json!(["src/big_class.rs"]));
assert_eq!(
doc["summary"],
"Orchestrates parsing; read this before the 3000-line body."
);
assert!(
doc["content"]
.as_str()
.is_some_and(|c| c.contains("3000-line body")),
"{doc:?}"
);
}
#[tokio::test]
async fn doc_tool_returns_docs_dir_doc_for_every_covered_file() {
let (_dir, cg) = setup_documented_project().await;
for file in ["src/search_es8.rs", "src/feed_es8.rs"] {
let result = handle_tool_call(&cg, "tokensave_doc", json!({"file": file}), None, None)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], true, "{file}: {parsed:?}");
let doc = &parsed["docs"][0];
assert_eq!(doc["doc_path"], "tokensave-docs/es8.md", "{file}");
assert_eq!(
doc["covers"],
json!(["src/feed_es8.rs", "src/search_es8.rs"]),
"{file}"
);
}
}
#[tokio::test]
async fn doc_tool_reports_no_doc_for_undocumented_file() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_doc",
json!({"file": "src/plain.rs"}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], false, "{parsed:?}");
assert_eq!(parsed["count"], 0);
assert_eq!(parsed["docs"], json!([]));
}
#[tokio::test]
async fn doc_tool_can_omit_content() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_doc",
json!({"file": "src/big_class.rs", "include_content": false}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
let doc = &parsed["docs"][0];
assert!(doc["content"].is_null(), "{doc:?}");
assert_eq!(doc["covers"], json!(["src/big_class.rs"]));
}
#[tokio::test]
async fn doc_tool_normalizes_backslash_paths() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_doc",
json!({"file": "src\\big_class.rs"}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], true, "{parsed:?}");
}
#[tokio::test]
async fn doc_staleness_is_unknown_without_git_history() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_doc",
json!({"file": "src/big_class.rs"}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert!(parsed["docs"][0]["doc_stale"].is_null(), "{parsed:?}");
}
#[tokio::test]
async fn doc_tool_is_registered_in_the_tool_list() {
let (_dir, cg) = setup_documented_project().await;
let defs = tokensave::mcp::tools::get_tool_definitions();
assert!(
defs.iter().any(|d| d.name == "tokensave_doc"),
"tokensave_doc must be advertised"
);
drop(cg);
}
#[tokio::test]
async fn entities_marks_files_that_have_companion_docs() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_entities",
json!({"file": "src/big_class.rs"}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], true, "{parsed:?}");
assert_eq!(parsed["doc_path"], json!(["src/big_class.readme.md"]));
assert!(parsed["doc_hint"].is_string(), "{parsed:?}");
}
#[tokio::test]
async fn entities_marks_undocumented_files_without_a_doc_path() {
let (_dir, cg) = setup_documented_project().await;
let result = handle_tool_call(
&cg,
"tokensave_entities",
json!({"file": "src/plain.rs"}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], false, "{parsed:?}");
assert!(parsed.get("doc_path").is_none(), "{parsed:?}");
}
#[tokio::test]
async fn doc_staleness_flags_code_committed_after_the_doc() {
let dir = TempDir::new().unwrap();
let project = dir.path();
let git = |args: &[&str]| {
let ok = std::process::Command::new("git")
.arg("-C")
.arg(project)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
assert!(ok, "git {args:?} failed");
};
if std::process::Command::new("git")
.arg("--version")
.output()
.is_err()
{
return; }
git(&["init", "-q"]);
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "t"]);
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/thing.rs"), "pub fn a() {}\n").unwrap();
fs::write(
project.join("src/thing.readme.md"),
"# Thing\n\nDescribes thing.\n",
)
.unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "doc and code together"]);
fs::write(
project.join("src/thing.rs"),
"pub fn a() {}\npub fn b() {}\n",
)
.unwrap();
git(&["add", "-A"]);
let future = std::process::Command::new("git")
.arg("-C")
.arg(project)
.args(["commit", "-qm", "code moved on"])
.env("GIT_COMMITTER_DATE", "2099-01-01T00:00:00 +0000")
.output()
.expect("commit");
assert!(future.status.success(), "{future:?}");
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
let result = handle_tool_call(
&cg,
"tokensave_doc",
json!({"file": "src/thing.rs", "include_content": false}),
None,
None,
)
.await
.unwrap();
let parsed: Value = serde_json::from_str(extract_text(&result.value)).unwrap();
assert_eq!(parsed["has_doc"], true, "{parsed:?}");
assert_eq!(
parsed["docs"][0]["doc_stale"], true,
"code committed after the doc must flag drift: {parsed:?}"
);
}