use serde::Serialize;
#[cfg(feature = "analysis")]
use crate::analysis::dead_code::{DeadCodeConfig, DeadCodeDetector, DeadCodeEntry};
#[cfg(feature = "analysis")]
use crate::diagnostics::{Diagnostic, Severity};
#[cfg(feature = "analysis")]
use crate::kit::{AsyncKit, AsyncReady, StorageModule};
#[cfg(all(test, feature = "cli", feature = "analysis"))]
use crate::model::EdgeType;
#[cfg(all(any(feature = "cli", feature = "mcp"), feature = "analysis"))]
use crate::service::error::kit_not_initialized;
#[cfg(all(any(feature = "cli", feature = "mcp"), feature = "analysis"))]
use crate::service::error::to_api_error;
#[cfg(feature = "analysis")]
use crate::service::error::CodeNexusError;
#[cfg(feature = "analysis")]
use crate::service::project::resolve_project_id;
#[cfg(all(any(feature = "cli", feature = "mcp"), feature = "analysis"))]
use crate::service::runtime::kit;
#[cfg(feature = "analysis")]
use crate::service::status::{git_head_commit, is_stale, resolve_project_root};
#[cfg(feature = "analysis")]
use crate::storage::StorageConfig;
#[cfg(all(any(feature = "cli", feature = "mcp"), feature = "analysis"))]
use sdforge::forge;
#[cfg(all(any(feature = "cli", feature = "mcp"), feature = "analysis"))]
use sdforge::prelude::ApiError;
#[cfg(feature = "analysis")]
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct DeadCodeOutput {
pub project: String,
pub dead_code: Vec<DeadCodeEntry>,
pub indexed_commit: String,
pub current_head: String,
pub is_stale: bool,
pub diagnostics: Vec<Diagnostic>,
}
#[cfg(feature = "analysis")]
fn stale_index_diagnostics(
project: &str,
root: &std::path::Path,
indexed_commit: &str,
current_head: &str,
) -> Vec<Diagnostic> {
if !is_stale(indexed_commit, current_head) {
return Vec::new();
}
vec![Diagnostic {
code: "index/stale".to_string(),
severity: Severity::Warning,
subject: project.to_string(),
message: format!(
"index was taken at {indexed_commit} but HEAD is now {current_head}; results may not reflect current source"
),
evidence: serde_json::json!({
"indexed_commit": indexed_commit,
"current_head": current_head,
}),
supported_fixes: vec![format!(
"Re-run: codenexus index --path {} --force true",
root.display()
)],
}]
}
#[cfg(feature = "analysis")]
#[derive(Debug, Clone)]
pub struct DeadCodeParams {
pub project: String,
pub entry: String,
pub check_exported: bool,
pub check_ffi: bool,
pub check_dynamic_dispatch: bool,
pub check_reflection: bool,
pub edge_types: String,
}
#[cfg(feature = "analysis")]
impl Default for DeadCodeParams {
fn default() -> Self {
Self {
project: String::new(),
entry: String::new(),
check_exported: true,
check_ffi: false,
check_dynamic_dispatch: false,
check_reflection: false,
edge_types: String::new(),
}
}
}
#[cfg(feature = "analysis")]
fn build_dead_code_config(params: &DeadCodeParams) -> DeadCodeConfig {
let default = DeadCodeConfig::default();
let final_edge_types =
crate::model::edge_type::parse_edge_type_list(¶ms.edge_types, &default.edge_types);
DeadCodeConfig {
check_exported: params.check_exported,
check_ffi: params.check_ffi,
check_dynamic_dispatch: params.check_dynamic_dispatch,
check_reflection: params.check_reflection,
edge_types: final_edge_types,
..default
}
}
#[cfg(feature = "analysis")]
pub fn run_dead_code(
kit: &AsyncKit<AsyncReady>,
params: &DeadCodeParams,
) -> Result<DeadCodeOutput, CodeNexusError> {
let storage = kit.require::<StorageModule>()?;
let project_id = resolve_project_id(&*storage, ¶ms.project)?;
let project_record = storage
.get_project(&project_id)
.map_err(CodeNexusError::from)?
.ok_or_else(|| CodeNexusError::ProjectNotFound(params.project.clone()))?;
let indexed_commit = project_record.last_commit.clone();
let storage_config = kit.config::<StorageConfig>()?;
let root = resolve_project_root(&project_record.root_path, &storage_config.db_path);
let current_head = git_head_commit(&root);
let stale = is_stale(&indexed_commit, ¤t_head);
let config = build_dead_code_config(params);
let detector = DeadCodeDetector::with_config(&*storage, config);
let mut entry_patterns: Vec<&str> = vec!["main", "Main", "__main__"];
let extras: Vec<String> = if params.entry.is_empty() {
Vec::new()
} else {
params
.entry
.split(',')
.map(|s| s.trim().to_string())
.collect()
};
for e in &extras {
entry_patterns.push(e.as_str());
}
let entries = detector.detect(&project_id, &entry_patterns)?;
let diagnostics =
stale_index_diagnostics(¶ms.project, &root, &indexed_commit, ¤t_head);
Ok(DeadCodeOutput {
project: params.project.clone(),
dead_code: entries,
indexed_commit,
current_head,
is_stale: stale,
diagnostics,
})
}
#[cfg(all(feature = "cli", feature = "analysis"))]
#[forge(
name = "dead_code",
version = "0.3.5",
description = "Detect unreferenced (dead) functions in a project.",
cli = true
)]
async fn dead_code(
project: String,
entry: String,
check_exported: bool,
check_ffi: bool,
check_dynamic_dispatch: bool,
check_reflection: bool,
edge_types: String,
) -> Result<(), ApiError> {
let kit = kit().ok_or_else(kit_not_initialized)?;
let params = DeadCodeParams {
project,
entry,
check_exported,
check_ffi,
check_dynamic_dispatch,
check_reflection,
edge_types,
};
let output = run_dead_code(&kit, ¶ms).map_err(|e| to_api_error(e, "dead_code_error"))?;
let json = serde_json::to_string(&output)
.map_err(|e| to_api_error(CodeNexusError::from(e), "dead_code_error"))?;
println!("{json}");
Ok(())
}
#[cfg(all(feature = "mcp", feature = "analysis"))]
#[forge(
name = "dead_code",
version = "0.3.5",
tool_name = "dead_code",
description = "Detect unreferenced (dead) functions with confidence levels and entry-point analysis. Params: project — name or id (required); entry — comma-separated extra entry-point patterns; check_exported — treat pub functions as live; check_ffi — treat FFI exports as live; check_dynamic_dispatch — treat trait-dispatch calls as live; check_reflection — treat reflection/derive-macro entry points as live; edge_types — comma-separated uppercase edge types (empty = defaults)."
)]
#[allow(clippy::too_many_arguments)]
async fn dead_code_mcp(
project: String,
entry: String,
check_exported: bool,
check_ffi: bool,
check_dynamic_dispatch: bool,
check_reflection: bool,
edge_types: String,
) -> Result<DeadCodeOutput, ApiError> {
let kit = kit().ok_or_else(kit_not_initialized)?;
let params = DeadCodeParams {
project,
entry,
check_exported,
check_ffi,
check_dynamic_dispatch,
check_reflection,
edge_types,
};
run_dead_code(&kit, ¶ms).map_err(|e| to_api_error(e, "dead_code_error"))
}
#[cfg(all(test, feature = "cli", feature = "analysis"))]
mod tests {
use super::*;
use crate::analysis::dead_code::Confidence;
use crate::kit::{build_kit, AsyncKit, AsyncReady, KitBootstrapConfig, StorageModule};
use crate::storage::capability::Storage;
use tempfile::TempDir;
fn fresh_db_path() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("svc_dead_code_testdb");
(dir, path)
}
fn build_kit_for_db(db: &std::path::Path) -> AsyncKit<AsyncReady> {
let config = KitBootstrapConfig::new(db.to_path_buf());
tokio::runtime::Runtime::new()
.unwrap()
.block_on(build_kit(&config))
.expect("build_kit")
}
fn seed_project(storage: &dyn Storage, id: &str, name: &str) {
storage
.execute(&format!(
"CREATE (:Project {{id: '{id}', name: '{name}', rootPath: '/demo', language: 'rust', fileCount: 1, indexedAt: 1000, lastCommit: 'abc'}});"
))
.expect("create project");
}
fn test_params() -> DeadCodeParams {
DeadCodeParams {
project: "demo".to_string(),
entry: String::new(),
check_exported: true,
check_ffi: true,
check_dynamic_dispatch: true,
check_reflection: false,
edge_types: String::new(),
}
}
fn cfg_params(
exported: bool,
ffi: bool,
dynamic: bool,
reflection: bool,
edges: &str,
) -> DeadCodeParams {
DeadCodeParams {
project: "demo".to_string(),
check_exported: exported,
check_ffi: ffi,
check_dynamic_dispatch: dynamic,
check_reflection: reflection,
edge_types: edges.to_string(),
..DeadCodeParams::default()
}
}
#[test]
fn run_succeeds_on_empty_db() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "demo", "demo");
let result = run_dead_code(&kit, &test_params());
assert!(result.is_ok(), "run should succeed: {:?}", result.err());
}
#[test]
fn run_returns_dead_function() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("require_storage");
seed_project(&*storage, "demo", "demo");
storage.execute("CREATE (:Function {id: 'f_foo', project: 'demo', name: 'foo', qualifiedName: 'demo.foo', filePath: '/src/lib.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create foo");
let result = run_dead_code(&kit, &test_params());
assert!(result.is_ok(), "run should succeed: {:?}", result.err());
}
#[test]
fn run_with_custom_entry_patterns() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("require_storage");
seed_project(&*storage, "demo", "demo");
storage.execute("CREATE (:Function {id: 'f_main', project: 'demo', name: 'main', qualifiedName: 'demo.main', filePath: '/src/main.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create main");
let result = run_dead_code(&kit, &{
let mut p = test_params();
p.entry = "custom_entry,other_entry".to_string();
p
});
assert!(result.is_ok(), "run should succeed: {:?}", result.err());
}
#[test]
fn output_serializes_to_json() {
let out = DeadCodeOutput {
project: "demo".into(),
dead_code: vec![DeadCodeEntry {
name: "foo".into(),
qualified_name: "demo.foo".into(),
file_path: "/src/lib.rs".into(),
start_line: 1,
language: "rust".into(),
reason: "zero incoming CALLS edges".into(),
confidence: Confidence::High,
}],
indexed_commit: "abc123".into(),
current_head: "def456".into(),
is_stale: true,
diagnostics: vec![],
};
let json = serde_json::to_string(&out).unwrap();
assert!(json.contains("\"project\":\"demo\""));
assert!(json.contains("\"dead_code\""));
assert!(json.contains("\"foo\""));
assert!(json.contains("\"indexed_commit\":\"abc123\""));
assert!(json.contains("\"current_head\":\"def456\""));
assert!(json.contains("\"is_stale\":true"));
assert!(json.contains("\"diagnostics\":[]"));
}
#[test]
fn stale_index_diagnostics_emit_on_stale_commit() {
let diags =
stale_index_diagnostics("demo", std::path::Path::new("/repo"), "abc123", "def456");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, "index/stale");
assert_eq!(diags[0].severity, Severity::Warning);
assert_eq!(diags[0].subject, "demo");
assert_eq!(diags[0].evidence["indexed_commit"], "abc123");
assert_eq!(diags[0].evidence["current_head"], "def456");
assert!(diags[0].supported_fixes[0].contains("--force true"));
}
#[test]
fn stale_index_diagnostics_empty_when_fresh_or_unknown() {
assert!(
stale_index_diagnostics("demo", std::path::Path::new("/repo"), "abc", "abc").is_empty()
);
assert!(stale_index_diagnostics("demo", std::path::Path::new("/repo"), "", "").is_empty());
assert!(
stale_index_diagnostics("demo", std::path::Path::new("/repo"), "abc", "").is_empty()
);
}
#[test]
fn run_dead_code_with_check_exported_excludes_exported() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("require_storage");
seed_project(&*storage, "demo", "demo");
storage.execute("CREATE (:Function {id: 'f_pub', project: 'demo', name: 'pub_fn', qualifiedName: 'demo.pub_fn', filePath: '/src/lib.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: true, docstring: '', content: '', parentQn: ''});").expect("create exported");
storage.execute("CREATE (:Function {id: 'f_priv', project: 'demo', name: 'priv_fn', qualifiedName: 'demo.priv_fn', filePath: '/src/lib.rs', startLine: 6, endLine: 10, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create private");
let output = run_dead_code(&kit, &test_params()).expect("run should succeed");
let names: Vec<&str> = output.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
!names.contains(&"pub_fn"),
"exported fn should be excluded with check_exported=true"
);
assert!(names.contains(&"priv_fn"), "private fn should be dead");
let output2 = run_dead_code(&kit, &{
let mut p = test_params();
p.check_exported = false;
p
})
.expect("run should succeed");
let names2: Vec<&str> = output2.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
names2.contains(&"pub_fn"),
"exported fn should be dead with check_exported=false"
);
assert!(
names2.contains(&"priv_fn"),
"private fn should still be dead"
);
}
#[test]
fn run_dead_code_with_check_ffi_excludes_ffi() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("require_storage");
seed_project(&*storage, "demo", "demo");
storage.execute("CREATE (:Function {id: 'f_ffi', project: 'demo', name: 'ffi_fn', qualifiedName: 'demo.ffi_fn', filePath: '/src/lib.rs', startLine: 1, endLine: 5, signature: 'extern \"C\" fn ffi_fn()', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create ffi");
storage.execute("CREATE (:Function {id: 'f_plain', project: 'demo', name: 'plain', qualifiedName: 'demo.plain', filePath: '/src/lib.rs', startLine: 6, endLine: 10, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create plain");
let output = run_dead_code(&kit, &test_params()).expect("run should succeed");
let names: Vec<&str> = output.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
!names.contains(&"ffi_fn"),
"FFI fn should be excluded with check_ffi=true"
);
assert!(names.contains(&"plain"), "plain fn should be dead");
let output2 = run_dead_code(&kit, &{
let mut p = test_params();
p.check_ffi = false;
p
})
.expect("run should succeed");
let names2: Vec<&str> = output2.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
names2.contains(&"ffi_fn"),
"FFI fn should be dead with check_ffi=false"
);
}
#[test]
fn run_dead_code_with_custom_edge_types() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("require_storage");
seed_project(&*storage, "demo", "demo");
storage.execute("CREATE (:Function {id: 'f_a', project: 'demo', name: 'a', qualifiedName: 'demo.a', filePath: '/src/a.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create a");
storage.execute("CREATE (:Function {id: 'f_b', project: 'demo', name: 'b', qualifiedName: 'demo.b', filePath: '/src/b.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create b");
storage.execute("CREATE (:CodeRelation {id: 'e1', source: 'f_a', target: 'f_b', type: 'USAGE', confidence: 1.0, confidenceTier: 'High', reason: '', startLine: 1, project: 'demo'});").expect("create edge");
let output = run_dead_code(&kit, &{
let mut p = test_params();
p.entry = "a".to_string();
p
})
.expect("run should succeed");
let names: Vec<&str> = output.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
!names.contains(&"b"),
"b should NOT be dead (reachable from seed a via USAGE)"
);
assert!(
!names.contains(&"a"),
"a should be alive (entry pattern seed)"
);
let output2 = run_dead_code(&kit, &{
let mut p = test_params();
p.entry = "a".to_string();
p.edge_types = "CALLS".to_string();
p
})
.expect("run should succeed");
let names2: Vec<&str> = output2.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
names2.contains(&"b"),
"b should be dead when only CALLS is checked (USAGE not traversed)"
);
assert!(
!names2.contains(&"a"),
"a should still be alive (entry pattern seed)"
);
}
#[test]
fn run_dead_code_returns_output_struct() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "demo", "demo");
let output = run_dead_code(&kit, &test_params()).expect("run should succeed");
assert_eq!(output.project, "demo");
assert!(
output.dead_code.is_empty(),
"empty DB should yield empty dead_code"
);
}
#[test]
fn build_dead_code_config_parses_edge_types() {
let config =
build_dead_code_config(&cfg_params(true, true, true, false, "CALLS,USAGE,TESTS"));
assert!(config.check_exported);
assert!(config.check_ffi);
assert!(config.check_dynamic_dispatch);
assert_eq!(config.edge_types.len(), 3);
assert!(config.edge_types.contains(&EdgeType::Calls));
assert!(config.edge_types.contains(&EdgeType::Usage));
assert!(config.edge_types.contains(&EdgeType::Tests));
}
#[test]
fn build_dead_code_config_empty_edge_types_uses_defaults() {
let config = build_dead_code_config(&cfg_params(true, true, true, false, ""));
assert!(config.check_exported);
assert!(config.check_ffi);
let default = DeadCodeConfig::default();
assert_eq!(config.edge_types, default.edge_types);
}
#[test]
fn build_dead_code_config_skips_invalid_edge_types() {
let config = build_dead_code_config(&cfg_params(
false,
false,
false,
false,
"CALLS,INVALID,TESTS",
));
assert!(!config.check_exported);
assert!(!config.check_ffi);
assert!(!config.check_dynamic_dispatch);
assert_eq!(config.edge_types.len(), 2);
assert!(config.edge_types.contains(&EdgeType::Calls));
assert!(config.edge_types.contains(&EdgeType::Tests));
}
#[test]
fn build_dead_code_config_all_invalid_keeps_defaults() {
let config =
build_dead_code_config(&cfg_params(true, true, true, false, "INVALID1,INVALID2"));
let default = DeadCodeConfig::default();
assert_eq!(
config.edge_types, default.edge_types,
"all-invalid should keep defaults"
);
}
#[test]
fn build_dead_code_config_trims_whitespace() {
let config =
build_dead_code_config(&cfg_params(true, true, true, false, " CALLS , USAGE "));
assert_eq!(config.edge_types.len(), 2);
assert!(config.edge_types.contains(&EdgeType::Calls));
assert!(config.edge_types.contains(&EdgeType::Usage));
}
#[test]
fn build_dead_code_config_passes_check_dynamic_dispatch_true() {
let config = build_dead_code_config(&cfg_params(true, true, true, false, ""));
assert!(
config.check_dynamic_dispatch,
"check_dynamic_dispatch=true should propagate"
);
}
#[test]
fn build_dead_code_config_passes_check_dynamic_dispatch_false() {
let config = build_dead_code_config(&cfg_params(true, true, false, false, ""));
assert!(
!config.check_dynamic_dispatch,
"check_dynamic_dispatch=false should propagate"
);
}
#[test]
fn run_dead_code_with_check_dynamic_dispatch_excludes_trait_impl() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("require_storage");
seed_project(&*storage, "demo", "demo");
storage.execute("CREATE (:Method {id: 'm_fmt', project: 'demo', name: 'fmt', qualifiedName: 'demo.src.lib.rs.fmt#Display', filePath: '/src/lib.rs', startLine: 5, endLine: 10, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create trait impl");
let output = run_dead_code(&kit, &test_params()).expect("run should succeed");
let names: Vec<&str> = output.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
!names.contains(&"fmt"),
"trait impl fmt#Display should NOT be dead with check_dynamic_dispatch=true"
);
let output2 = run_dead_code(&kit, &{
let mut p = test_params();
p.check_dynamic_dispatch = false;
p
})
.expect("run should succeed");
let names2: Vec<&str> = output2.dead_code.iter().map(|e| e.name.as_str()).collect();
assert!(
names2.contains(&"fmt"),
"trait impl fmt#Display IS dead with check_dynamic_dispatch=false"
);
}
#[serial_test::serial(kit_init)]
#[test]
fn dead_code_wrapper_succeeds_via_init_kit() {
use crate::service::runtime::{init_kit, reset_kit_for_testing};
reset_kit_for_testing();
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project(&*storage, "demo", "demo");
init_kit(kit).expect("init_kit");
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(dead_code(
"demo".to_string(),
"".to_string(),
false,
false,
false,
false,
"".to_string(),
));
assert!(result.is_ok(), "wrapper should succeed: {:?}", result.err());
reset_kit_for_testing();
}
#[serial_test::serial(kit_init)]
#[test]
fn dead_code_wrapper_fails_when_kit_not_initialized() {
use crate::service::runtime::reset_kit_for_testing;
reset_kit_for_testing();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(dead_code(
"demo".to_string(),
"".to_string(),
false,
false,
false,
false,
"".to_string(),
));
assert!(result.is_err(), "wrapper should fail without kit");
reset_kit_for_testing();
}
fn seed_project_with(
storage: &dyn Storage,
id: &str,
name: &str,
root_path: &str,
last_commit: &str,
) {
use crate::storage::schema::escape_cypher_string;
storage
.execute(&format!(
"CREATE (:Project {{id: '{}', name: '{}', rootPath: '{}', language: 'rust', fileCount: 0, indexedAt: 1000, lastCommit: '{}'}});",
escape_cypher_string(id),
escape_cypher_string(name),
escape_cypher_string(root_path),
escape_cypher_string(last_commit),
))
.expect("create project");
}
#[test]
fn test_dead_code_output_includes_indexed_commit_when_set() {
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project_with(&*storage, "demo", "demo", "/nonexistent/path", "abc123");
let output = run_dead_code(&kit, &test_params()).expect("run");
assert_eq!(output.indexed_commit, "abc123");
assert_eq!(output.current_head, "", "non-git root → empty current_head");
assert!(!output.is_stale, "current_head empty → not stale");
}
#[test]
fn test_is_stale_true_when_commit_differs() {
let tmp = TempDir::new().unwrap();
let status = std::process::Command::new("git")
.arg("init")
.arg(tmp.path())
.status();
if status.is_err() || !status.unwrap().success() {
eprintln!("skipping test: git init failed");
return;
}
let git = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(tmp.path())
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false)
};
std::fs::write(tmp.path().join("README.md"), "init\n").unwrap();
if !git(&["add", "."])
|| !git(&[
"-c",
"user.email=t@t.com",
"-c",
"user.name=t",
"commit",
"-m",
"init",
])
{
eprintln!("skipping test: git commit failed");
return;
}
let head = std::process::Command::new("git")
.arg("-C")
.arg(tmp.path())
.arg("rev-parse")
.arg("HEAD")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
if head.is_empty() {
eprintln!("skipping test: could not determine HEAD");
return;
}
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
let root = tmp.path().to_string_lossy().into_owned();
seed_project_with(&*storage, "demo", "demo", &root, "abc123");
let output = run_dead_code(&kit, &test_params()).expect("run");
assert_eq!(output.indexed_commit, "abc123");
assert_eq!(output.current_head, head);
assert!(output.is_stale, "commits differ → stale");
}
#[test]
fn test_is_stale_false_when_commits_match() {
let tmp = TempDir::new().unwrap();
let status = std::process::Command::new("git")
.arg("init")
.arg(tmp.path())
.status();
if status.is_err() || !status.unwrap().success() {
eprintln!("skipping test: git init failed");
return;
}
let git = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(tmp.path())
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false)
};
std::fs::write(tmp.path().join("README.md"), "init\n").unwrap();
if !git(&["add", "."])
|| !git(&[
"-c",
"user.email=t@t.com",
"-c",
"user.name=t",
"commit",
"-m",
"init",
])
{
eprintln!("skipping test: git commit failed");
return;
}
let head = std::process::Command::new("git")
.arg("-C")
.arg(tmp.path())
.arg("rev-parse")
.arg("HEAD")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
if head.is_empty() {
eprintln!("skipping test: could not determine HEAD");
return;
}
let (_dir, db) = fresh_db_path();
let kit = build_kit_for_db(&db);
let storage = kit.require::<StorageModule>().expect("storage");
let root = tmp.path().to_string_lossy().into_owned();
seed_project_with(&*storage, "demo", "demo", &root, &head);
let output = run_dead_code(&kit, &test_params()).expect("run");
assert_eq!(output.indexed_commit, head);
assert_eq!(output.current_head, head);
assert!(!output.is_stale, "commits match → fresh");
}
#[test]
fn test_dead_code_resolves_relative_rootpath_via_db_path() {
let project_root = TempDir::new().unwrap();
let project_root_path = project_root.path().canonicalize().unwrap();
let status = std::process::Command::new("git")
.arg("init")
.arg(&project_root_path)
.status();
if status.is_err() || !status.unwrap().success() {
eprintln!("skipping test: git init failed");
return;
}
let git = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(&project_root_path)
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false)
};
std::fs::write(project_root_path.join("README.md"), "init\n").unwrap();
if !git(&["add", "."])
|| !git(&[
"-c",
"user.email=t@t.com",
"-c",
"user.name=t",
"commit",
"-m",
"init",
])
{
eprintln!("skipping test: git commit failed");
return;
}
let head = std::process::Command::new("git")
.arg("-C")
.arg(&project_root_path)
.arg("rev-parse")
.arg("HEAD")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
if head.is_empty() {
eprintln!("skipping test: could not determine HEAD");
return;
}
let db_dir = project_root_path.join(".codenexus");
std::fs::create_dir_all(&db_dir).unwrap();
let db_path = db_dir.join("test.lbug");
let kit = build_kit_for_db(&db_path);
let storage = kit.require::<StorageModule>().expect("storage");
seed_project_with(&*storage, "demo", "demo", ".", &head);
let output = run_dead_code(&kit, &test_params()).expect("run");
assert_eq!(
output.current_head, head,
"current_head must be the project's actual HEAD, not the CWD's HEAD"
);
assert!(
!output.is_stale,
"should not be stale: indexed_commit == current_head after fallback resolution"
);
}
}