use super::super::test_helpers::{make_scope, make_server_with_lawyer, make_temp_workspace};
use super::*;
use crate::server::PathfinderServer;
use pathfinder_common::config::PathfinderConfig;
use pathfinder_common::sandbox::Sandbox;
use pathfinder_common::types::{DegradedReason, WorkspaceRoot};
use pathfinder_lsp::types::ReferenceLocation;
use pathfinder_lsp::{DefinitionLocation, MockLawyer};
use pathfinder_search::MockScout;
use pathfinder_treesitter::mock::MockSurgeon;
use std::sync::Arc;
#[tokio::test]
async fn test_find_all_references_lsp_returns_references() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/auth.rs".into(),
line: 10,
column: 4,
snippet: "fn login() {".into(),
},
ReferenceLocation {
file: "src/main.rs".into(),
line: 20,
column: 8,
snippet: "login();".into(),
},
]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 2, "should have 2 references");
assert!(!val.degraded, "should not be degraded when LSP works");
}
#[tokio::test]
async fn test_find_all_references_respects_max_references() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
let refs: Vec<_> = (0..5)
.map(|i| ReferenceLocation {
file: format!("src/file{i}.rs"),
line: u32::try_from(i + 1).unwrap(),
column: 1,
snippet: format!("// reference {i}"),
})
.collect();
lawyer.set_references_result(Ok(refs));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 3, offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
let refs = val.references.unwrap_or_default();
assert_eq!(
refs.len(),
3,
"should return exactly max_results=3 references, got {}",
refs.len()
);
}
#[tokio::test]
async fn test_find_all_references_degraded_when_no_lsp() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(pathfinder_lsp::NoOpLawyer);
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
Arc::new(MockScout::default()),
surgeon,
lawyer,
);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded, "should be degraded when LSP unavailable");
assert_eq!(val.degraded_reason, Some(DegradedReason::NoLsp));
assert_eq!(val.lsp_readiness, Some("unavailable".to_owned()));
assert!(val.references.is_none());
assert_eq!(
val.resolution_strategy,
Some("treesitter_fallback".to_owned())
);
}
#[tokio::test]
async fn test_find_all_references_lsp_error_degraded() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Err("protocol error".to_string()));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded, "should be degraded on LSP error");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspErrorGrepFallback)
);
assert_eq!(val.lsp_readiness, Some("unavailable".to_owned()));
assert!(val.references.is_none());
assert_eq!(
val.resolution_strategy,
Some("treesitter_fallback".to_owned())
);
}
#[tokio::test]
async fn test_find_all_references_connection_lost_degraded() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_lsp_error(Err(LspError::ConnectionLost));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded, "should be degraded on connection lost");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspErrorGrepFallback)
);
assert_eq!(val.lsp_readiness, Some("unavailable".to_owned()));
assert!(val.references.is_none());
assert_eq!(
val.resolution_strategy,
Some("treesitter_fallback".to_owned())
);
}
#[tokio::test]
async fn test_find_all_references_with_implementations_and_references() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/main.rs".into(),
line: 10,
column: 8,
snippet: "login();".into(),
},
ReferenceLocation {
file: "src/tests.rs".into(),
line: 5,
column: 4,
snippet: "let _ = login();".into(),
},
]));
lawyer.set_goto_implementation_result(Ok(vec![DefinitionLocation {
file: "src/auth_impl.rs".into(),
line: 15,
column: 4,
preview: "impl LoginService for AuthService {".into(),
}]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert_eq!(val.total_references, Some(3));
assert_eq!(val.files_referenced, 3);
let refs = val.references.unwrap_or_default();
assert_eq!(refs[0].file, "src/auth_impl.rs");
assert_eq!(refs[1].file, "src/main.rs");
assert_eq!(refs[2].file, "src/tests.rs");
}
#[tokio::test]
async fn test_find_all_references_offset_skips_implementations() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_implementation_result(Ok(vec![
DefinitionLocation {
file: "src/auth_impl1.rs".into(),
line: 10,
column: 4,
preview: "impl1".into(),
},
DefinitionLocation {
file: "src/auth_impl2.rs".into(),
line: 20,
column: 4,
preview: "impl2".into(),
},
]));
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/main.rs".into(),
line: 10,
column: 8,
snippet: "login1();".into(),
},
ReferenceLocation {
file: "src/tests.rs".into(),
line: 5,
column: 4,
snippet: "login2();".into(),
},
ReferenceLocation {
file: "src/app.rs".into(),
line: 15,
column: 8,
snippet: "login3();".into(),
},
]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 2,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 3, "should return all 3 references");
assert_eq!(refs[0].file, "src/main.rs");
assert_eq!(refs[1].file, "src/tests.rs");
assert_eq!(refs[2].file, "src/app.rs");
assert_eq!(val.total_references, Some(5)); }
#[tokio::test]
async fn test_find_all_references_offset_past_implementations_paginates_references() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_implementation_result(Ok(vec![DefinitionLocation {
file: "src/auth_impl.rs".into(),
line: 10,
column: 4,
preview: "impl".into(),
}]));
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/file1.rs".into(),
line: 10,
column: 8,
snippet: "ref1".into(),
},
ReferenceLocation {
file: "src/file2.rs".into(),
line: 20,
column: 8,
snippet: "ref2".into(),
},
ReferenceLocation {
file: "src/file3.rs".into(),
line: 30,
column: 8,
snippet: "ref3".into(),
},
ReferenceLocation {
file: "src/file4.rs".into(),
line: 40,
column: 8,
snippet: "ref4".into(),
},
ReferenceLocation {
file: "src/file5.rs".into(),
line: 50,
column: 8,
snippet: "ref5".into(),
},
]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 2,
offset: 3,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 2, "should return 2 references");
assert_eq!(refs[0].file, "src/file3.rs");
assert_eq!(refs[1].file, "src/file4.rs");
assert_eq!(val.total_references, Some(6)); assert!(val.truncated, "should be truncated");
}
#[tokio::test]
async fn test_find_all_references_zero_references_zero_implementations() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![]));
lawyer.set_goto_implementation_result(Ok(vec![]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(!val.degraded);
assert_eq!(val.total_references, Some(0));
assert_eq!(val.files_referenced, 0);
assert!(val.references.unwrap_or_default().is_empty());
}
#[tokio::test]
async fn test_find_all_references_rejects_sandbox_denied_path() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "/etc/passwd::function".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
assert!(
result.is_err(),
"should return error for sandbox denied path"
);
}
#[tokio::test]
async fn test_find_all_references_implementation_error_references_ok() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![ReferenceLocation {
file: "src/main.rs".into(),
line: 10,
column: 8,
snippet: "login();".into(),
}]));
lawyer.set_goto_implementation_result(Err(LspError::Protocol(
"implementation error".to_string(),
)));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(!val.degraded);
assert_eq!(val.total_references, Some(1));
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].file, "src/main.rs");
}
#[tokio::test]
async fn test_find_all_references_large_offset_returns_empty() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/main.rs".into(),
line: 10,
column: 8,
snippet: "login1();".into(),
},
ReferenceLocation {
file: "src/tests.rs".into(),
line: 5,
column: 4,
snippet: "login2();".into(),
},
ReferenceLocation {
file: "src/app.rs".into(),
line: 15,
column: 8,
snippet: "login3();".into(),
},
]));
lawyer.set_goto_implementation_result(Ok(vec![
DefinitionLocation {
file: "src/impl1.rs".into(),
line: 10,
column: 4,
preview: "impl1".into(),
},
DefinitionLocation {
file: "src/impl2.rs".into(),
line: 20,
column: 4,
preview: "impl2".into(),
},
DefinitionLocation {
file: "src/impl3.rs".into(),
line: 30,
column: 4,
preview: "impl3".into(),
},
]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 100,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
let refs = val.references.unwrap_or_default();
assert!(
refs.is_empty(),
"should return empty when offset past total, got {}",
refs.len()
);
assert_eq!(val.total_references, Some(6));
assert!(
!val.truncated,
"should NOT be truncated when offset past total"
);
}
#[tokio::test]
async fn test_find_all_references_truncation_boundary() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_implementation_result(Ok(vec![
DefinitionLocation {
file: "src/impl1.rs".into(),
line: 10,
column: 4,
preview: "impl1".into(),
},
DefinitionLocation {
file: "src/impl2.rs".into(),
line: 20,
column: 4,
preview: "impl2".into(),
},
]));
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/ref1.rs".into(),
line: 10,
column: 8,
snippet: "ref1".into(),
},
ReferenceLocation {
file: "src/ref2.rs".into(),
line: 20,
column: 8,
snippet: "ref2".into(),
},
ReferenceLocation {
file: "src/ref3.rs".into(),
line: 30,
column: 8,
snippet: "ref3".into(),
},
]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 5,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert_eq!(val.total_references, Some(5));
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 5, "should return all 5 items");
assert!(
!val.truncated,
"should NOT be truncated when exactly at boundary"
);
}
#[tokio::test]
async fn test_find_all_references_deduplicates_impl_and_refs() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_implementation_result(Ok(vec![DefinitionLocation {
file: "src/auth_impl.rs".into(),
line: 15,
column: 4,
preview: "impl LoginService for AuthService {".into(),
}]));
lawyer.set_references_result(Ok(vec![
ReferenceLocation {
file: "src/auth_impl.rs".into(),
line: 15, column: 4,
snippet: "impl LoginService for AuthService {".into(),
},
ReferenceLocation {
file: "src/main.rs".into(),
line: 10,
column: 8,
snippet: "login();".into(),
},
]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert_eq!(
val.total_references,
Some(2),
"duplicate (file,line) should be deduped"
);
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].file, "src/auth_impl.rs"); assert_eq!(refs[1].file, "src/main.rs"); }
#[tokio::test]
async fn test_find_all_references_grep_fallback_returns_results_with_mock_scout() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.extend([Ok(None), Ok(None)]);
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![
pathfinder_search::SearchMatch {
file: "src/auth.rs".to_string(), line: 1,
column: 4,
content: "fn login() -> bool { true }".to_string(), context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:a".to_string(),
known: Some(false),
},
pathfinder_search::SearchMatch {
file: "src/main.rs".to_string(), line: 10,
column: 8,
content: "let _ = login();".to_string(), context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:b".to_string(),
known: Some(false),
},
],
total_matches: 2,
truncated: false,
files_searched: 2,
files_in_scope: 2,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded, "should still be degraded (grep is heuristic)");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::NoLspGrepFallback),
"degraded_reason should be NoLspGrepFallback when grep returns results"
);
assert!(
val.references.is_some(),
"references should be Some when grep fallback finds results"
);
let refs = val.references.unwrap();
assert_eq!(
refs.len(),
1,
"should have exactly 1 reference (definition file excluded)"
);
assert_eq!(refs[0].file, "src/main.rs");
assert_eq!(refs[0].line, 10);
assert_eq!(val.files_referenced, 1);
assert_eq!(val.total_references, Some(1));
assert_eq!(val.resolution_strategy, Some("grep_file_scoped".to_owned()));
assert_eq!(val.lsp_readiness, Some("unavailable".to_owned()));
}
#[tokio::test]
async fn test_find_all_references_grep_fallback_no_results_stays_none() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.push(Ok(None));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/auth.rs".to_string(), line: 1,
column: 4,
content: "fn login() -> bool { true }".to_string(), context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:a".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded);
assert_eq!(
val.degraded_reason,
Some(DegradedReason::NoLsp),
"should be NoLsp when grep finds no valid references"
);
assert!(
val.references.is_none(),
"references should be None when grep finds no valid refs"
);
assert_eq!(val.files_referenced, 0);
assert!(val.total_references.is_none());
}
#[tokio::test]
async fn test_find_all_references_lsp_error_uses_grep_fallback() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.push(Ok(None));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/main.rs".to_string(), line: 10,
column: 8,
content: "login();".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:test".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_lsp_error(Err(LspError::ConnectionLost));
let server = PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded);
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspErrorGrepFallback)
);
assert!(val.references.is_some());
let refs = val.references.unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].file, "src/main.rs");
}
#[tokio::test]
async fn test_find_all_references_lsp_timeout_uses_grep_fallback() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.push(Ok(None));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/main.rs".to_string(),
line: 10,
column: 8,
content: "login();".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:test".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_lsp_error(Err(LspError::Timeout {
operation: "references".to_string(),
timeout_ms: 5000,
}));
let server = PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded);
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspTimeoutGrepFallback)
);
assert_eq!(val.lsp_readiness, Some("warming_up".to_owned()));
assert_eq!(val.warm_start_in_progress, Some(true));
assert_eq!(val.resolution_strategy, Some("grep_file_scoped".to_owned()));
assert!(val.references.is_some());
let refs = val.references.unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].file, "src/main.rs");
}
#[tokio::test]
#[allow(clippy::too_many_lines)] async fn test_find_all_references_grep_filters_non_source_files() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.extend([Ok(None), Ok(None), Ok(None), Ok(None)]);
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![
pathfinder_search::SearchMatch {
file: "src/main.rs".to_string(),
line: 10,
column: 8,
content: "login();".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:a".to_string(),
known: Some(false),
},
pathfinder_search::SearchMatch {
file: "web/auth.ts".to_string(),
line: 5,
column: 4,
content: "import { login }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:b".to_string(),
known: Some(false),
},
pathfinder_search::SearchMatch {
file: "docs/README.md".to_string(),
line: 20,
column: 1,
content: "call login()".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:c".to_string(),
known: Some(false),
},
pathfinder_search::SearchMatch {
file: "config.json".to_string(),
line: 3,
column: 1,
content: "\"login\": true".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:d".to_string(),
known: Some(false),
},
],
total_matches: 4,
truncated: false,
files_searched: 4,
files_in_scope: 4,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded);
assert_eq!(val.degraded_reason, Some(DegradedReason::NoLspGrepFallback));
assert!(val.references.is_some());
let refs = val.references.unwrap();
assert_eq!(
refs.len(),
2,
"should filter out non-source files (.md, .json)"
);
let files: std::collections::HashSet<_> = refs.iter().map(|r| r.file.as_str()).collect();
assert!(files.contains("src/main.rs"));
assert!(files.contains("web/auth.ts"));
assert!(!files.contains("docs/README.md"));
assert!(!files.contains("config.json"));
}
#[tokio::test]
async fn test_find_all_references_grep_fallback_unsupported_ext_uses_line_number() {
let surgeon = Arc::new(MockSurgeon::new());
let vue_scope = pathfinder_common::types::SymbolScope {
content: "<script setup>const useAuth = () => {}</script>".to_owned(),
start_line: 4, end_line: 4,
name_column: 20, language: "vue".to_owned(),
};
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(vue_scope));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.extend([Ok(None), Ok(None)]);
let ws_dir = tempfile::tempdir().expect("temp dir");
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
let components_dir = ws_dir.path().join("src/components");
std::fs::create_dir_all(&components_dir).unwrap();
std::fs::write(
components_dir.join("Auth.vue"),
"<script setup>\nconst useAuth = () => {}\n</script>\n<template>\n <div @click=\"useAuth()\">Login</div>\n</template>",
).unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![
pathfinder_search::SearchMatch {
file: "src/components/Auth.vue".to_string(),
line: 5, column: 20,
content: "const useAuth = () => {}".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:a".to_string(),
known: Some(false),
},
pathfinder_search::SearchMatch {
file: "src/components/Auth.vue".to_string(),
line: 8, column: 15,
content: "<div @click=\"useAuth()\">Login</div>".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:b".to_string(),
known: Some(false),
},
],
total_matches: 2,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = crate::server::types::TraceParams {
semantic_path: "src/components/Auth.vue::useAuth".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded);
assert_eq!(val.degraded_reason, Some(DegradedReason::NoLspGrepFallback));
assert_eq!(val.resolution_strategy, Some("grep_file_scoped".to_owned()));
assert!(
val.references.is_some(),
"should have references - same-file different-line refs should be kept"
);
let refs = val.references.unwrap();
assert_eq!(
refs.len(),
1,
"BUG 1: expected exactly 1 reference (def site excluded, same-file diff-line ref kept)"
);
assert_eq!(refs[0].file, "src/components/Auth.vue");
assert_eq!(
refs[0].line, 8,
"should be the reference at line 8, not the definition at line 5"
);
}
#[tokio::test]
async fn test_grep_references_fallback_regex_compilation_warning() {
let surgeon = Arc::new(MockSurgeon::default());
let lawyer = Arc::new(pathfinder_lsp::MockLawyer::default());
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let definition_path = std::path::Path::new("src/main.invalid_regex");
let definition_scope = super::super::test_helpers::make_scope();
let params = crate::server::types::TraceParams {
semantic_path: "src/main.invalid_regex::main".to_string(),
max_references: 100,
offset: 0,
..Default::default()
};
let res = server
.grep_references_fallback("test_symbol", definition_path, &definition_scope, ¶ms)
.await;
assert!(res.is_none());
}
#[tokio::test]
async fn test_grep_references_fallback_overflows() {
let surgeon = Arc::new(MockSurgeon::default());
let lawyer = Arc::new(pathfinder_lsp::MockLawyer::default());
let scout = Arc::new(pathfinder_search::MockScout::default());
let ws_dir = crate::server::tools::navigation::test_helpers::make_temp_workspace();
let ws = pathfinder_common::types::WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = pathfinder_common::config::PathfinderConfig::default();
let sandbox = pathfinder_common::sandbox::Sandbox::new(ws.path(), &config.sandbox);
let server = crate::server::PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout.clone(),
surgeon.clone(),
lawyer,
);
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/main.rs".to_string(),
line: u64::MAX,
column: u64::MAX,
content: "fn main() {}".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "hash".to_string(),
known: None,
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.push(Ok(None));
let definition_path = std::path::Path::new("src/main.rs");
let definition_scope = super::super::test_helpers::make_scope();
let params = crate::server::types::TraceParams {
semantic_path: "src/main.rs::main".to_string(),
max_references: 100,
offset: 0,
..Default::default()
};
let fallback_res = server
.grep_references_fallback("test_symbol", definition_path, &definition_scope, ¶ms)
.await;
assert!(fallback_res.is_some());
let (refs, files_count) = fallback_res.unwrap();
assert_eq!(files_count, 1);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].line, 1);
assert_eq!(refs[0].column, 1);
}
#[tokio::test]
async fn test_find_all_references_file_read_failure_and_open_doc_failure() {
let surgeon = Arc::new(MockSurgeon::default());
let lawyer = Arc::new(pathfinder_lsp::MockLawyer::default());
lawyer
.did_open_error
.lock()
.unwrap()
.replace(pathfinder_lsp::LspError::ConnectionLost);
let ws_dir = crate::server::tools::navigation::test_helpers::make_temp_workspace();
let ws = pathfinder_common::types::WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = pathfinder_common::config::PathfinderConfig::default();
let sandbox = pathfinder_common::sandbox::Sandbox::new(ws.path(), &config.sandbox);
let server = crate::server::PathfinderServer::with_all_engines(
ws,
config,
sandbox,
std::sync::Arc::new(pathfinder_search::MockScout::default()),
surgeon.clone(),
lawyer.clone(),
);
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(super::super::test_helpers::make_scope()));
let file_path = ws_dir.path().join("src/main.rs");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o000)).unwrap();
}
let params = crate::server::types::TraceParams {
semantic_path: "src/main.rs::main".to_string(),
max_references: 100,
offset: 0,
..Default::default()
};
lawyer.references_result.lock().unwrap().replace(Ok(vec![
pathfinder_lsp::types::ReferenceLocation {
file: "src/user.rs".to_string(),
line: 2,
column: 1,
snippet: "use main;".to_string(),
},
]));
lawyer
.goto_implementation_result
.lock()
.unwrap()
.replace(Err(pathfinder_lsp::LspError::ConnectionLost));
let call_res = server.find_all_references_impl(params).await;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o755));
}
let call_res_unwrapped = call_res.expect("should succeed despite warnings");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res_unwrapped.structured_content.unwrap()).unwrap();
assert!(!val.degraded);
assert_eq!(val.references.unwrap().len(), 1);
}
#[tokio::test]
async fn test_find_all_references_zero_results_but_resolvable_definition_triggers_grep_fallback() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.enclosing_symbol_detail_results
.lock()
.unwrap()
.push(Ok(None));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/main.rs".to_string(),
line: 10,
column: 8,
content: "login();".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:test".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![]));
lawyer.set_goto_implementation_result(Ok(vec![]));
lawyer.set_goto_definition_result(Ok(Some(DefinitionLocation {
file: "src/auth.rs".into(),
line: 1,
column: 4,
preview: "fn login() -> bool { true }".into(),
})));
let server = PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(val.degraded);
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspWarmupGrepFallback)
);
let refs = val.references.unwrap_or_default();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].file, "src/main.rs");
}
#[tokio::test]
async fn test_hint_present_when_zero_references_non_degraded() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![]));
lawyer.set_goto_implementation_result(Ok(vec![]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(!val.degraded, "should not be degraded");
assert_eq!(val.total_references, Some(0));
assert!(
val.hint.is_some(),
"hint must be present when zero references found (non-degraded)"
);
let hint = val.hint.unwrap();
assert!(
hint.contains("zero references"),
"hint must mention zero references, got: {hint}"
);
}
#[tokio::test]
async fn test_hint_absent_when_references_exist() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_references_result(Ok(vec![ReferenceLocation {
file: "src/main.rs".into(),
line: 5,
column: 10,
snippet: "login()".into(),
}]));
lawyer.set_goto_implementation_result(Ok(vec![]));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = crate::server::types::TraceParams {
semantic_path: "src/auth.rs::login".to_owned(),
max_references: 50,
offset: 0,
..Default::default()
};
let result = server.find_all_references_impl(params).await;
let call_res = result.expect("should succeed");
let val: crate::server::types::FindAllReferencesMetadata =
serde_json::from_value(call_res.structured_content.unwrap()).unwrap();
assert!(!val.degraded, "should not be degraded");
assert!(
val.hint.is_none(),
"hint must be absent when references exist"
);
}