use std::fs;
use tempfile::tempdir;
use scryer_db::{DependencyPackage, Project, ProjectDependency, ScryerDb, SourceFile, Symbol};
use scryer_engine::EngineService;
#[tokio::test]
async fn test_dependency_ingestion_and_deduplication() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
fs::write(
root.join("Cargo.toml"),
r#"[package]
name = "fixture-app"
version = "0.1.0"
edition = "2021"
[dependencies]
bitflags = "2.4"
"#,
)?;
fs::write(root.join("src/lib.rs"), "pub fn run() {}")?;
let db = ScryerDb::new_in_memory().await?;
let mut conn = db.lock().await;
let project1 = Project::create()
.slug("test-project-1".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?;
drop(conn);
let engine = EngineService::new(db.clone());
let count1 = engine.index_dependencies(project1.id, root).await?;
assert!(count1 > 0, "Should have indexed dependencies for project 1");
let (pkg_count, file_count, sym_count, proj_dep_count_1) = {
let mut conn = db.lock().await;
let packages = DependencyPackage::all().exec(&mut *conn).await?;
assert!(!packages.is_empty(), "DependencyPackage rows must exist");
let project_deps = ProjectDependency::all().exec(&mut *conn).await?;
assert!(
!project_deps.is_empty(),
"ProjectDependency rows must exist"
);
assert!(
project_deps.iter().all(|d| d.project_id == project1.id),
"All project_deps must point to project 1"
);
let files = SourceFile::filter_by_project_id(0).exec(&mut *conn).await?;
assert!(
!files.is_empty(),
"Dependency SourceFile rows with project_id = 0 must exist"
);
assert!(
files.iter().all(|f| f.dependency_package_id.is_some()),
"Dependency files must have dependency_package_id set"
);
let symbols = Symbol::filter_by_project_id(0).exec(&mut *conn).await?;
assert!(
!symbols.is_empty(),
"Dependency Symbol rows with project_id = 0 must exist"
);
(
packages.len(),
files.len(),
symbols.len(),
project_deps.len(),
)
};
let count2 = engine.index_dependencies(project1.id, root).await?;
assert_eq!(
count2, 0,
"Re-index should find all packages cached and parse 0 new packages"
);
{
let mut conn = db.lock().await;
let packages_after = DependencyPackage::all().exec(&mut *conn).await?;
assert_eq!(
packages_after.len(),
pkg_count,
"Package count must not change on re-index"
);
let files_after = SourceFile::filter_by_project_id(0).exec(&mut *conn).await?;
assert_eq!(
files_after.len(),
file_count,
"Dependency file count must not change on re-index"
);
let symbols_after = Symbol::filter_by_project_id(0).exec(&mut *conn).await?;
assert_eq!(
symbols_after.len(),
sym_count,
"Dependency symbol count must not change on re-index"
);
let project_deps_after = ProjectDependency::all().exec(&mut *conn).await?;
assert_eq!(
project_deps_after.len(),
proj_dep_count_1,
"ProjectDependency count must not change on re-index"
);
}
let temp2 = tempdir()?;
let root2 = temp2.path();
fs::create_dir_all(root2.join("src"))?;
fs::write(
root2.join("Cargo.toml"),
r#"[package]
name = "fixture-app-2"
version = "0.1.0"
edition = "2021"
[dependencies]
bitflags = "2.4"
"#,
)?;
fs::write(root2.join("src/lib.rs"), "pub fn app2() {}")?;
let mut conn = db.lock().await;
let project2 = Project::create()
.slug("test-project-2".to_string())
.root_path(root2.to_string_lossy().to_string())
.default_branch("main".to_string())
.is_active(false)
.created_at("2026-10-07T00:00:00Z".to_string())
.updated_at("2026-10-07T00:00:00Z".to_string())
.exec(&mut *conn)
.await?;
drop(conn);
let count3 = engine.index_dependencies(project2.id, root2).await?;
assert_eq!(
count3, 0,
"Project 2 should reuse cached packages without re-parsing"
);
{
let mut conn = db.lock().await;
let packages_final = DependencyPackage::all().exec(&mut *conn).await?;
assert_eq!(
packages_final.len(),
pkg_count,
"Package count must remain identical when second project links dependencies"
);
let files_final = SourceFile::filter_by_project_id(0).exec(&mut *conn).await?;
assert_eq!(
files_final.len(),
file_count,
"Dependency SourceFile count must remain identical"
);
let symbols_final = Symbol::filter_by_project_id(0).exec(&mut *conn).await?;
assert_eq!(
symbols_final.len(),
sym_count,
"Dependency Symbol count must remain identical"
);
let project_deps_final = ProjectDependency::all().exec(&mut *conn).await?;
assert_eq!(
project_deps_final.len(),
proj_dep_count_1 * 2,
"ProjectDependency rows should double to link both projects to shared packages"
);
let p2_deps: Vec<_> = project_deps_final
.iter()
.filter(|d| d.project_id == project2.id)
.collect();
assert_eq!(p2_deps.len(), proj_dep_count_1);
}
Ok(())
}
#[tokio::test]
async fn test_incomplete_dependency_package_is_reindexed() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
fs::write(
root.join("Cargo.toml"),
r#"[package]
name = "fixture-app"
version = "0.1.0"
edition = "2021"
[dependencies]
bitflags = "2.4"
"#,
)?;
fs::write(root.join("src/lib.rs"), "pub fn run() {}")?;
let db = ScryerDb::new_in_memory().await?;
let project = {
let mut conn = db.lock().await;
Project::create()
.slug("test-incomplete-dep".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?
};
let engine = EngineService::new(db.clone());
assert!(engine.index_dependencies(project.id, root).await? > 0);
let (files_before, symbols_before) = {
let mut conn = db.lock().await;
let packages = DependencyPackage::all().exec(&mut *conn).await?;
assert!(!packages.is_empty());
assert!(
packages.iter().all(|p| !p.indexed_at.is_empty()),
"completed packages must be stamped"
);
(
SourceFile::filter_by_project_id(0)
.exec(&mut *conn)
.await?
.len(),
Symbol::filter_by_project_id(0)
.exec(&mut *conn)
.await?
.len(),
)
};
{
let mut conn = db.lock().await;
let mut pkg = DependencyPackage::all()
.exec(&mut *conn)
.await?
.into_iter()
.next()
.unwrap();
pkg.update()
.indexed_at(String::new())
.exec(&mut *conn)
.await?;
}
let retried = engine.index_dependencies(project.id, root).await?;
assert_eq!(retried, 1, "only the incomplete package is re-indexed");
let mut conn = db.lock().await;
let packages = DependencyPackage::all().exec(&mut *conn).await?;
assert!(packages.iter().all(|p| !p.indexed_at.is_empty()));
assert_eq!(
SourceFile::filter_by_project_id(0)
.exec(&mut *conn)
.await?
.len(),
files_before,
"retry must replace files, not duplicate them"
);
assert_eq!(
Symbol::filter_by_project_id(0)
.exec(&mut *conn)
.await?
.len(),
symbols_before,
"retry must replace symbols, not duplicate them"
);
Ok(())
}
#[tokio::test]
async fn test_removed_dependency_link_is_pruned() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
let manifest = |deps: &str| {
format!(
"[package]\nname = \"fixture-app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n{deps}"
)
};
fs::write(root.join("Cargo.toml"), manifest("bitflags = \"2.4\"\n"))?;
fs::write(root.join("src/lib.rs"), "pub fn run() {}")?;
let db = ScryerDb::new_in_memory().await?;
let project = {
let mut conn = db.lock().await;
Project::create()
.slug("test-prune-links".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?
};
let engine = EngineService::new(db.clone());
engine.index_dependencies(project.id, root).await?;
{
let mut conn = db.lock().await;
assert!(!ProjectDependency::all().exec(&mut *conn).await?.is_empty());
}
fs::write(root.join("Cargo.toml"), manifest(""))?;
engine.index_dependencies(project.id, root).await?;
let mut conn = db.lock().await;
assert!(
ProjectDependency::all().exec(&mut *conn).await?.is_empty(),
"link to the removed dependency must be pruned"
);
assert!(
!DependencyPackage::all().exec(&mut *conn).await?.is_empty(),
"shared package rows are kept"
);
Ok(())
}