mod common;
use anyhow::Result;
use common::McpTestClient;
use parking_lot::Mutex;
use serde_json::{Value, json};
use std::sync::OnceLock;
fn sqry_index_exists() -> bool {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
let workspace_root = std::path::Path::new(&manifest_dir)
.parent()
.unwrap_or(std::path::Path::new("."));
workspace_root
.join(".sqry")
.join("graph")
.join("snapshot.sqry")
.exists()
}
macro_rules! require_sqry_index {
() => {
if !sqry_index_exists() {
eprintln!("Skipping: no .sqry/graph/snapshot.sqry found");
return Ok(());
}
};
}
fn shared_initialized_client() -> parking_lot::MutexGuard<'static, McpTestClient> {
static CLIENT: OnceLock<Mutex<McpTestClient>> = OnceLock::new();
let mutex = CLIENT.get_or_init(|| {
Mutex::new(
McpTestClient::new_initialized().expect("Failed to create shared e2e test client"),
)
});
mutex.lock()
}
fn validate_and_extract_response(response: &Value) -> Result<String> {
assert_eq!(response["jsonrpc"], "2.0", "Invalid JSON-RPC version");
if response["error"].is_object() {
let error_msg = response["error"]["message"]
.as_str()
.unwrap_or("unknown error");
return Ok(format!("[Error response: {error_msg}]"));
}
let content = &response["result"]["content"];
anyhow::ensure!(content.is_array(), "Response content must be an array");
anyhow::ensure!(
!content.as_array().unwrap().is_empty(),
"Content array is empty"
);
let text = content[0]["text"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("No text field in content"))?;
Ok(text.to_string())
}
#[test]
fn test_e2e_semantic_search_graph_builders() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "semantic_search",
"arguments": {
"query": "GraphBuilder implementations",
"max_results": 10
}
}),
100,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return search results");
Ok(())
}
#[test]
fn test_e2e_pattern_search_functions() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "pattern_search",
"arguments": {
"pattern": "add_method",
"max_results": 20
}
}),
101,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
text.contains("add_method") || text.contains("matches") || text.contains("Error"),
"Should find matching symbols or return error"
);
Ok(())
}
#[test]
fn test_e2e_document_symbols() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_document_symbols",
"arguments": {
"file_path": "src/lib.rs"
}
}),
102,
)?;
let text = validate_and_extract_response(&response)?;
assert!(text.len() > 100, "Should return substantial symbol list");
Ok(())
}
#[test]
fn test_e2e_workspace_symbols_search() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_workspace_symbols",
"arguments": {
"query": "GraphBuildHelper",
"max_results": 5
}
}),
103,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return symbol results");
Ok(())
}
#[test]
fn test_e2e_graph_statistics() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_graph_stats",
"arguments": {}
}),
104,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
text.contains("totalNodes"),
"Should show node count. Got: {text}"
);
assert!(
text.contains("totalEdges"),
"Should show edge count. Got: {text}"
);
assert!(
text.contains("totalFiles"),
"Should show file count. Got: {text}"
);
Ok(())
}
#[test]
fn test_e2e_index_status() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_index_status",
"arguments": {}
}),
105,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
text.contains("Index") || text.contains("status") || text.contains("version"),
"Should return index metadata"
);
Ok(())
}
#[test]
fn test_e2e_find_definition() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_definition",
"arguments": {
"symbol": "GraphBuildHelper"
}
}),
106,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return definition result");
Ok(())
}
#[test]
fn test_e2e_find_references() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_references",
"arguments": {
"symbol": "add_function",
"max_results": 20
}
}),
107,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return reference results");
Ok(())
}
#[test]
fn test_e2e_hierarchical_search() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "hierarchical_search",
"arguments": {
"query": "build graph",
"max_results": 10
}
}),
108,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
!text.is_empty(),
"Should return hierarchical search results"
);
Ok(())
}
#[test]
fn test_e2e_list_indexed_files() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "list_files",
"arguments": {
"language": "rust",
"max_results": 100
}
}),
109,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
text.contains(".rs") || text.contains("file") || text.contains("Rust"),
"Should return Rust file listings"
);
Ok(())
}
#[test]
fn test_e2e_relation_query_callers() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "relation_query",
"arguments": {
"symbol": "build_graph",
"relation_type": "callers",
"max_depth": 1
}
}),
110,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return relation query result");
Ok(())
}
#[test]
fn test_e2e_list_symbols_by_kind() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "list_symbols",
"arguments": {
"kind": "function",
"max_results": 20
}
}),
111,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return symbol list");
Ok(())
}
#[test]
fn test_e2e_explain_code_with_context() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "explain_code",
"arguments": {
"file_path": "sqry-core/src/graph/unified/mod.rs",
"symbol_name": "CodeGraph",
"include_context": true,
"include_relations": true
}
}),
112,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return code explanation");
Ok(())
}
#[test]
fn test_e2e_cross_language_analysis() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "cross_language_edges",
"arguments": {
"max_results": 10
}
}),
113,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
!text.is_empty(),
"Should return cross-language analysis result"
);
Ok(())
}
#[test]
fn test_e2e_dependency_analysis() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "show_dependencies",
"arguments": {
"symbol_name": "CodeGraph",
"max_depth": 2
}
}),
114,
)?;
let text = validate_and_extract_response(&response)?;
assert!(!text.is_empty(), "Should return dependency tree");
Ok(())
}
#[test]
#[allow(clippy::similar_names)] fn test_e2e_index_status_file_count_accuracy() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let status_response = client.call(
"tools/call",
json!({
"name": "get_index_status",
"arguments": {}
}),
115,
)?;
let status_text = validate_and_extract_response(&status_response)?;
#[allow(clippy::similar_names)] let stats_response = client.call(
"tools/call",
json!({
"name": "get_graph_stats",
"arguments": {}
}),
116,
)?;
let stats_text = validate_and_extract_response(&stats_response)?;
let status_json: Value = serde_json::from_str(&status_text)
.map_err(|e| anyhow::anyhow!("Failed to parse index status JSON: {e}"))?;
let stats_json: Value = serde_json::from_str(&stats_text)
.map_err(|e| anyhow::anyhow!("Failed to parse graph stats JSON: {e}"))?;
let files_indexed = status_json["data"]["filesIndexed"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid filesIndexed in status response"))?;
let total_files = stats_json["data"]["totalFiles"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid totalFiles in stats response"))?;
assert_eq!(
files_indexed, total_files,
"Index status filesIndexed ({files_indexed}) should match graph stats totalFiles ({total_files})"
);
assert!(
files_indexed > 0,
"File count should be greater than 0, got {files_indexed}"
);
Ok(())
}
#[test]
#[ignore = "Expensive rebuild test - enable for validation testing"]
fn test_e2e_rebuild_index_file_count_accuracy() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let rebuild_response = client.call(
"tools/call",
json!({
"name": "rebuild_index",
"arguments": {
"force": true
}
}),
117,
)?;
let rebuild_text = validate_and_extract_response(&rebuild_response)?;
let rebuild_json: Value = serde_json::from_str(&rebuild_text)
.map_err(|e| anyhow::anyhow!("Failed to parse rebuild response JSON: {e}"))?;
let files_indexed = rebuild_json["data"]["filesIndexed"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid filesIndexed in rebuild response"))?;
let stats_response = client.call(
"tools/call",
json!({
"name": "get_graph_stats",
"arguments": {}
}),
118,
)?;
let stats_text = validate_and_extract_response(&stats_response)?;
let stats_json: Value = serde_json::from_str(&stats_text)
.map_err(|e| anyhow::anyhow!("Failed to parse graph stats JSON: {e}"))?;
let total_files = stats_json["data"]["totalFiles"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid totalFiles in stats response"))?;
assert_eq!(
files_indexed, total_files,
"Rebuild response filesIndexed ({files_indexed}) should match graph stats totalFiles ({total_files})"
);
assert!(
files_indexed > 0,
"Rebuild file count should be greater than 0, got {files_indexed}"
);
Ok(())
}
#[test]
fn test_e2e_rebuild_index_existing_index_file_count() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let rebuild_response = client.call(
"tools/call",
json!({
"name": "rebuild_index",
"arguments": {
"force": false
}
}),
119,
)?;
let rebuild_text = validate_and_extract_response(&rebuild_response)?;
let rebuild_json: Value = serde_json::from_str(&rebuild_text).map_err(|e| {
anyhow::anyhow!("Failed to parse rebuild response JSON: {e} | Text was: {rebuild_text}")
})?;
let files_indexed = rebuild_json["data"]["filesIndexed"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid filesIndexed in rebuild response"))?;
let stats_response = client.call(
"tools/call",
json!({
"name": "get_graph_stats",
"arguments": {}
}),
120,
)?;
let stats_text = validate_and_extract_response(&stats_response)?;
let stats_json: Value = serde_json::from_str(&stats_text)
.map_err(|e| anyhow::anyhow!("Failed to parse graph stats JSON: {e}"))?;
let total_files = stats_json["data"]["totalFiles"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid totalFiles in stats response"))?;
assert_eq!(
files_indexed, total_files,
"Rebuild (no force) filesIndexed ({files_indexed}) should match graph stats totalFiles ({total_files})"
);
let success = rebuild_json["data"]["success"]
.as_bool()
.ok_or_else(|| anyhow::anyhow!("Missing or invalid success field"))?;
assert!(success, "Rebuild should indicate success");
Ok(())
}
#[test]
#[allow(clippy::items_after_statements)] fn test_e2e_index_status_manifest_only_fallback() -> Result<()> {
require_sqry_index!();
use sqry_core::graph::unified::persistence::{
BuildProvenance, MANIFEST_SCHEMA_VERSION, Manifest, SNAPSHOT_FORMAT_VERSION,
};
use std::collections::HashMap;
use tempfile::TempDir;
let temp_dir = TempDir::new()?;
let graph_dir = temp_dir.path().join(".sqry").join("graph");
std::fs::create_dir_all(&graph_dir)?;
let mut file_count_map = HashMap::new();
file_count_map.insert("rust".to_string(), 100);
file_count_map.insert("python".to_string(), 50);
file_count_map.insert("javascript".to_string(), 75);
let expected_total: usize = file_count_map.values().sum();
let manifest = Manifest {
schema_version: MANIFEST_SCHEMA_VERSION,
snapshot_format_version: SNAPSHOT_FORMAT_VERSION,
built_at: chrono::Utc::now().to_rfc3339(),
root_path: temp_dir.path().to_string_lossy().to_string(),
node_count: 1000,
edge_count: 2000,
raw_edge_count: None,
snapshot_sha256: "test_checksum".to_string(),
build_provenance: BuildProvenance {
sqry_version: "3.2.0".to_string(),
build_timestamp: chrono::Utc::now().to_rfc3339(),
build_command: "test".to_string(),
plugin_hashes: HashMap::new(),
},
file_count: file_count_map,
languages: vec![
"rust".to_string(),
"python".to_string(),
"javascript".to_string(),
],
config: HashMap::new(),
confidence: HashMap::new(),
last_indexed_commit: None,
plugin_selection: None,
};
let manifest_path = graph_dir.join("manifest.json");
manifest.save(&manifest_path)?;
let snapshot_path = graph_dir.join("snapshot.sqry");
std::fs::write(&snapshot_path, b"corrupted_data")?;
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_index_status",
"arguments": {
"path": temp_dir.path().to_str().unwrap()
}
}),
121,
)?;
let text = validate_and_extract_response(&response)?;
let status_json: Value = serde_json::from_str(&text)
.map_err(|e| anyhow::anyhow!("Failed to parse index status JSON: {e}"))?;
let files_indexed = status_json["data"]["filesIndexed"]
.as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing filesIndexed in response"))?;
assert_eq!(
files_indexed, expected_total as u64,
"filesIndexed should equal manifest.file_count sum when snapshot header is unreadable"
);
let has_index = status_json["data"]["hasIndex"]
.as_bool()
.ok_or_else(|| anyhow::anyhow!("Missing hasIndex in response"))?;
assert!(has_index, "Should report index exists");
Ok(())
}
#[test]
fn test_e2e_direct_callers_suffix_match() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "direct_callers",
"arguments": {
"symbol": "CondensationDag::build_with_budget"
}
}),
3001,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
!text.contains("Symbol not found"),
"direct_callers should find 'CondensationDag::build_with_budget' via suffix matching"
);
Ok(())
}
#[test]
fn test_e2e_direct_callees_suffix_match() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "direct_callees",
"arguments": {
"symbol": "CondensationDag::build_with_budget"
}
}),
3002,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
!text.contains("Symbol not found"),
"direct_callees should find 'CondensationDag::build_with_budget' via suffix matching"
);
Ok(())
}
#[test]
fn test_e2e_get_hover_info_suffix_match() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_hover_info",
"arguments": {
"symbol": "CondensationDag::build_with_budget"
}
}),
3003,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
!text.is_empty(),
"get_hover_info should find 'CondensationDag::build_with_budget' via suffix matching"
);
Ok(())
}
#[test]
fn test_e2e_get_references_suffix_match() -> Result<()> {
require_sqry_index!();
let mut client = shared_initialized_client();
let response = client.call(
"tools/call",
json!({
"name": "get_references",
"arguments": {
"symbol": "CondensationDag::build_with_budget",
"max_results": 10
}
}),
3004,
)?;
let text = validate_and_extract_response(&response)?;
assert!(
!text.contains("No references found"),
"get_references should find 'CondensationDag::build_with_budget' via suffix matching"
);
Ok(())
}