use std::fs;
use tempfile::tempdir;
use scryer_db::{DependencyPackage, Project, ProjectDependency, ScryerDb, SourceFile, Symbol};
use scryer_engine::EngineService;
use scryer_engine::search::{SearchQuery, SearchResults, SearchScope};
async fn test_project(db: &ScryerDb, root: &std::path::Path) -> anyhow::Result<Project> {
let mut conn = db.lock().await;
let project = Project::create()
.slug("test-search".to_string())
.root_path(root.to_string_lossy().to_string())
.default_branch("main".to_string())
.is_active(true)
.created_at("2026-10-07T00:00:00Z".to_string())
.updated_at("2026-10-07T00:00:00Z".to_string())
.exec(&mut *conn)
.await?;
Ok(project)
}
fn text(q: &str) -> SearchQuery {
SearchQuery {
text: Some(q.to_string()),
limit: 20,
..SearchQuery::default()
}
}
fn names(res: &SearchResults) -> Vec<&str> {
res.hits.iter().map(|h| h.name.as_str()).collect()
}
#[tokio::test]
async fn test_symbol_search_ranking_like_symbol_and_generations() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
fs::write(
root.join("src/net.rs"),
r#"
/// Retry an operation with exponential backoff.
pub fn retry_with_backoff(attempts: u32) -> u32 {
attempts
}
/// Exponential backoff policy.
pub struct ExponentialBackoff {
pub base_ms: u64,
}
"#,
)?;
fs::write(
root.join("src/config.rs"),
r#"
/// Parse the configuration file.
pub fn parse_config(path: &str) -> String {
path.to_string()
}
"#,
)?;
let db = ScryerDb::new_in_memory().await?;
let project = test_project(&db, root).await?;
let engine = EngineService::new(db.clone());
engine.index_project(project.id, root).await?;
let res = engine
.search_symbols(project.id, text("backoff retry"))
.await?;
let top: Vec<&str> = names(&res).into_iter().take(2).collect();
assert!(top.contains(&"retry_with_backoff"), "got {top:?}");
assert!(top.contains(&"ExponentialBackoff"), "got {top:?}");
assert!(!names(&res).contains(&"parse_config"));
assert_eq!(res.hits[0].name, "retry_with_backoff");
assert!(res.hits[0].matched_terms.contains(&"retry".to_string()));
let res = engine
.search_symbols(
project.id,
SearchQuery {
kinds: vec!["STRUCT".to_string()],
..text("backoff")
},
)
.await?;
assert_eq!(names(&res), vec!["ExponentialBackoff"]);
let res = engine
.search_symbols(
project.id,
SearchQuery {
file_filter: Some("CONFIG".to_string()),
..text("parse backoff")
},
)
.await?;
assert_eq!(names(&res), vec!["parse_config"]);
let res = engine
.search_symbols(
project.id,
SearchQuery {
like_symbol: Some("retry_with_backoff".to_string()),
limit: 20,
..SearchQuery::default()
},
)
.await?;
assert_eq!(res.hits[0].name, "ExponentialBackoff");
assert!(!names(&res).contains(&"retry_with_backoff"));
assert!(res.like_symbol.is_some());
let res = engine
.search_symbols(
project.id,
SearchQuery {
offset: 1,
limit: 1,
..text("backoff")
},
)
.await?;
assert_eq!(res.total, 2);
assert_eq!(res.hits.len(), 1);
assert!(!res.has_more);
fs::write(
root.join("src/jitter.rs"),
"pub fn jittered_backoff() -> u64 { 0 }\n",
)?;
engine.index_project(project.id, root).await?;
let res = engine.search_symbols(project.id, text("backoff")).await?;
assert!(names(&res).contains(&"jittered_backoff"));
fs::remove_file(root.join("src/net.rs"))?;
engine.index_project(project.id, root).await?;
let res = engine.search_symbols(project.id, text("backoff")).await?;
assert_eq!(names(&res), vec!["jittered_backoff"]);
assert!(
engine
.search_symbols(project.id, SearchQuery::default())
.await
.is_err()
);
Ok(())
}
#[tokio::test]
async fn test_dependency_scope_uses_project_links() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
fs::write(root.join("src/lib.rs"), "pub fn local_mutex() {}\n")?;
let db = ScryerDb::new_in_memory().await?;
let project = test_project(&db, root).await?;
let engine = EngineService::new(db.clone());
engine.index_project(project.id, root).await?;
{
let mut guard = db.lock().await;
for (crate_name, linked) in [("tokio_mock", true), ("other_mock", false)] {
let pkg = DependencyPackage::create()
.name(crate_name.to_string())
.version("1.0.0".to_string())
.source_type("cratesio".to_string())
.package_hash(format!("hash-{crate_name}"))
.root_path(root.to_string_lossy().to_string())
.manifest_path(root.join("Cargo.toml").to_string_lossy().to_string())
.indexed_at("2026-10-07T00:00:00Z".to_string())
.exec(&mut *guard)
.await?;
if linked {
ProjectDependency::create()
.project_id(project.id)
.dependency_package_id(pkg.id)
.is_direct(true)
.features(String::new())
.exec(&mut *guard)
.await?;
}
let file = SourceFile::create()
.project_id(0)
.path("src/sync.rs".to_string())
.language("rust".to_string())
.content_hash(format!("file-{crate_name}"))
.line_count(10)
.byte_size(100)
.dependency_package_id(Some(pkg.id))
.exec(&mut *guard)
.await?;
Symbol::create()
.project_id(0)
.file_id(file.id)
.name("Mutex".to_string())
.qualified_name(format!("{crate_name}::sync::Mutex"))
.kind("struct".to_string())
.visibility("public".to_string())
.signature("pub struct Mutex<T>".to_string())
.start_line(1)
.end_line(5)
.start_byte(0)
.end_byte(50)
.dependency_package_id(Some(pkg.id))
.exec(&mut *guard)
.await?;
}
}
let deps = |crate_name: Option<&str>| SearchQuery {
scope: SearchScope::Dependencies {
crate_name: crate_name.map(str::to_string),
},
..text("Mutex")
};
let res = engine.search_symbols(project.id, deps(None)).await?;
assert_eq!(res.total, 1);
assert_eq!(res.hits[0].crate_name.as_deref(), Some("tokio_mock"));
assert_eq!(res.hits[0].version.as_deref(), Some("1.0.0"));
let res = engine
.search_symbols(project.id, deps(Some("OTHER_MOCK")))
.await?;
assert_eq!(res.total, 1);
assert_eq!(res.hits[0].crate_name.as_deref(), Some("other_mock"));
let res = engine
.search_symbols(project.id, deps(Some("missing")))
.await?;
assert_eq!(res.total, 0);
let res = engine.search_symbols(project.id, text("mutex")).await?;
assert_eq!(names(&res), vec!["local_mutex"]);
Ok(())
}
#[test]
fn test_code_is_recognised_by_path_and_module() {
use scryer_engine::search::cache::is_test_symbol;
for path in [
"tests/test_engine.py",
"scrapy/tests/helpers.py",
"pkg/test_utils.py",
"pkg/utils_test.py",
"pkg/conftest.py",
"packages/server/src/foo.test.ts",
"packages/server/src/foo.spec.tsx",
"packages/server/src/__tests__/foo.ts",
"crates/a/tests/integration.rs",
"crates/a/benches/speed.rs",
"examples/demo.rs",
"src\\test\\helpers.rs",
] {
assert!(is_test_symbol(path, "m.f"), "{path} is test code");
}
for path in [
"scrapy/core/engine.py",
"scrapy/utils/test.py",
"scrapy/contest.py",
"packages/server/src/latest.ts",
"packages/server/src/testing.ts",
"src/attest.rs",
"src/lib.rs",
] {
assert!(!is_test_symbol(path, "m.f"), "{path} is source");
}
assert!(is_test_symbol(
"src/lib.rs",
"crate::buffer::tests::set_string"
));
assert!(is_test_symbol("src/lib.rs", "crate::test::helper"));
assert!(!is_test_symbol("src/lib.rs", "crate::buffer::set_string"));
assert!(!is_test_symbol("src/lib.rs", "crate::contests::run"));
}
#[tokio::test]
async fn test_code_ranks_below_source_for_the_same_match_but_is_still_found() -> anyhow::Result<()>
{
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("pkg"))?;
fs::create_dir_all(root.join("tests"))?;
fs::write(
root.join("tests/test_handlers.py"),
"def download_handler_scheme():\n return 1\n\ndef test_only_in_tests_scheme():\n return 2\n",
)?;
fs::write(
root.join("pkg/handlers.py"),
"def download_handler_scheme():\n return 3\n",
)?;
let db = ScryerDb::new_in_memory().await?;
let project = test_project(&db, root).await?;
let engine = EngineService::new(db.clone());
engine.index_project(project.id, root).await?;
let res = engine
.search_symbols(project.id, text("download handler scheme"))
.await?;
assert_eq!(res.hits[0].file_path, "pkg/handlers.py", "{:?}", res.hits);
assert_eq!(res.hits[1].file_path, "tests/test_handlers.py");
let res = engine
.search_symbols(project.id, text("test only in tests"))
.await?;
assert_eq!(res.hits[0].name, "test_only_in_tests_scheme");
Ok(())
}