use super::capability::Storage;
use super::error::Result;
use super::schema::escape_identifier;
use crate::model::NodeLabel;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QualityViolation {
pub rule: &'static str,
pub message: String,
pub project: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct QualityReport {
pub violations: Vec<QualityViolation>,
}
impl QualityReport {
#[must_use]
pub fn is_clean(&self) -> bool {
self.violations.is_empty()
}
#[must_use]
pub fn count_for_rule(&self, rule: &str) -> usize {
self.violations.iter().filter(|v| v.rule == rule).count()
}
}
pub struct QualityChecker<'a> {
storage: &'a dyn Storage,
}
impl<'a> QualityChecker<'a> {
#[must_use]
pub fn new(storage: &'a dyn Storage) -> Self {
Self { storage }
}
pub fn run_all(&self) -> Result<QualityReport> {
let mut report = QualityReport::default();
if let Ok(v) = self.check_fqn_uniqueness() {
report.violations.extend(v);
}
if let Ok(v) = self.check_edge_integrity() {
report.violations.extend(v);
}
if let Ok(v) = self.check_project_isolation() {
report.violations.extend(v);
}
if let Ok(v) = self.check_hash_integrity() {
report.violations.extend(v);
}
Ok(report)
}
pub fn check_fqn_uniqueness(&self) -> Result<Vec<QualityViolation>> {
let mut seen: std::collections::HashMap<(String, String), Vec<String>> =
std::collections::HashMap::new();
for label in NodeLabel::all() {
if label == NodeLabel::Project {
continue;
}
let table = escape_identifier(label.table_name());
let cypher = format!(
"MATCH (n:{table}) \
WHERE n.qualifiedName IS NOT NULL AND n.project IS NOT NULL \
RETURN n.project AS project, n.qualifiedName AS qn, n.id AS id;"
);
let Ok(rows) = self.storage.query(&cypher) else {
continue;
};
for row in rows {
let project = row
.first()
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let qn = row
.get(1)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let id = row
.get(2)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !project.is_empty() && !qn.is_empty() {
seen.entry((project, qn)).or_default().push(id);
}
}
}
let mut violations: Vec<QualityViolation> = seen
.into_iter()
.filter(|(_, ids)| ids.len() > 1)
.map(|((project, qn), ids)| QualityViolation {
rule: "DQ-002",
message: format!(
"Duplicate FQN '{}' in project '{}' ({} nodes: {})",
qn,
project,
ids.len(),
ids.join(", ")
),
project: Some(project),
})
.collect();
violations.sort_by(|a, b| a.message.cmp(&b.message));
Ok(violations)
}
pub fn check_edge_integrity(&self) -> Result<Vec<QualityViolation>> {
let mut all_node_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
for label in NodeLabel::all() {
let table = escape_identifier(label.table_name());
let cypher = format!("MATCH (n:{table}) RETURN n.id AS id;");
let Ok(rows) = self.storage.query(&cypher) else {
continue;
};
for row in rows {
if let Some(id) = row.first().and_then(|v| v.as_str()) {
all_node_ids.insert(id.to_string());
}
}
}
let cypher = "MATCH (r:CodeRelation) RETURN r.source AS source, r.target AS target, r.project AS project;";
let rows = self.storage.query(cypher)?;
let mut violations = Vec::new();
for row in &rows {
let source = row.first().and_then(|v| v.as_str()).unwrap_or("");
let target = row.get(1).and_then(|v| v.as_str()).unwrap_or("");
let project = row
.get(2)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !all_node_ids.contains(source) {
violations.push(QualityViolation {
rule: "DQ-004",
message: format!("Orphan edge: source '{}' does not exist", source),
project: Some(project.clone()),
});
}
if !all_node_ids.contains(target) {
violations.push(QualityViolation {
rule: "DQ-004",
message: format!("Orphan edge: target '{}' does not exist", target),
project: Some(project),
});
}
}
violations.sort_by(|a, b| a.message.cmp(&b.message));
Ok(violations)
}
pub fn check_project_isolation(&self) -> Result<Vec<QualityViolation>> {
let projects = self.storage.list_projects()?;
let project_ids: Vec<String> = projects.into_iter().map(|p| p.id).collect();
let mut violations = Vec::new();
for label in NodeLabel::all() {
if label == NodeLabel::Project {
continue;
}
let table = escape_identifier(label.table_name());
let mut per_project_count: i64 = 0;
for pid in &project_ids {
let escaped = escape_cypher(pid);
let cypher = format!(
"MATCH (n:{table}) WHERE n.project = '{escaped}' RETURN count(n) AS cnt;"
);
let Ok(rows) = self.storage.query(&cypher) else {
continue;
};
if let Some(cnt) = rows
.first()
.and_then(|r| r.first())
.and_then(|v| v.as_i64())
{
per_project_count += cnt;
}
}
let cypher = format!("MATCH (n:{table}) RETURN count(n) AS cnt;");
let Ok(rows) = self.storage.query(&cypher) else {
continue;
};
if let Some(total) = rows
.first()
.and_then(|r| r.first())
.and_then(|v| v.as_i64())
{
if total != per_project_count {
violations.push(QualityViolation {
rule: "DQ-005",
message: format!(
"Project isolation violation in {}: total {} vs per-project sum {}",
label.table_name(),
total,
per_project_count
),
project: None,
});
}
}
}
Ok(violations)
}
pub fn check_hash_integrity(&self) -> Result<Vec<QualityViolation>> {
let cypher = "MATCH (f:File) RETURN f.id AS id, f.project AS project, f.hash AS hash;";
let rows = self.storage.query(cypher)?;
let violations: Vec<QualityViolation> = rows
.iter()
.filter(|row| {
let hash = row.get(2).and_then(|v| v.as_str()).unwrap_or("");
hash.is_empty()
})
.map(|row| {
let id = row.first().and_then(|v| v.as_str()).unwrap_or("");
let project = row
.get(1)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
QualityViolation {
rule: "DQ-006",
message: format!("File node '{}' has empty hash", id),
project: Some(project),
}
})
.collect();
Ok(violations)
}
}
fn escape_cypher(s: &str) -> String {
s.replace('\\', "\\\\").replace('\'', "\\'")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{EdgeType, Language};
use std::sync::Arc;
fn fresh_storage() -> Arc<dyn Storage> {
crate::storage::StorageModule::build_cap(&crate::storage::StorageConfig::in_memory())
.expect("StorageModule::build_cap")
}
fn sample_project(id: &str, name: &str) -> crate::model::Node {
crate::model::Node::builder(NodeLabel::Project, name, name)
.id(id)
.language(Language::Rust)
.properties(serde_json::json!({
"rootPath": "/repo/".to_string() + name,
"fileCount": 10,
"indexedAt": 1_700_000_000,
}))
.build()
}
fn sample_file(id: &str, project: &str, path: &str, hash: &str) -> crate::model::Node {
crate::model::Node::builder(NodeLabel::File, path, path)
.id(id)
.project(project)
.file_path(path)
.language(Language::Rust)
.properties(serde_json::json!({"hash": hash, "lineCount": 100}))
.build()
}
fn sample_function(id: &str, project: &str, name: &str, qn: &str) -> crate::model::Node {
crate::model::Node::builder(NodeLabel::Function, name, qn)
.id(id)
.project(project)
.file_path("/src/main.rs")
.start_line(1)
.end_line(10)
.signature("fn main()")
.build()
}
#[test]
fn test_dq002_detects_duplicate_fqn() {
let storage = fresh_storage();
let nodes = vec![
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "other", "demo.main"),
];
storage
.save_nodes(&nodes, NodeLabel::Function)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert_eq!(
violations.len(),
1,
"expected exactly one DQ-002 violation, got {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-002");
assert!(violations[0].message.contains("demo.main"));
assert!(violations[0].message.contains("demo"));
assert_eq!(violations[0].project.as_deref(), Some("demo"));
}
#[test]
fn test_dq002_clean_when_unique() {
let storage = fresh_storage();
let nodes = vec![
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "helper", "demo.helper"),
];
storage
.save_nodes(&nodes, NodeLabel::Function)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert!(
violations.is_empty(),
"expected no DQ-002 violations, got {violations:?}"
);
}
#[test]
fn test_dq002_same_fqn_in_different_projects_is_not_duplicate() {
let storage = fresh_storage();
let nodes = vec![
sample_function("f1", "alpha", "main", "alpha.main"),
sample_function("f2", "beta", "main", "alpha.main"),
];
storage
.save_nodes(&nodes, NodeLabel::Function)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert!(violations.is_empty(), "got {violations:?}");
}
#[test]
fn test_dq004_detects_orphan_edge() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_edges(&[
crate::model::Edge::builder("f1", "f2_missing", EdgeType::Calls, "demo").build(),
])
.expect("save_edges");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
1,
"expected one DQ-004 violation for orphan target, got {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-004");
assert!(violations[0].message.contains("f2_missing"));
}
#[test]
fn test_dq004_clean_when_all_edges_valid() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "helper", "demo.helper"),
],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_edges(&[crate::model::Edge::builder("f1", "f2", EdgeType::Calls, "demo").build()])
.expect("save_edges");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert!(
violations.is_empty(),
"expected no DQ-004 violations, got {violations:?}"
);
}
#[test]
fn test_dq004_detects_orphan_source_and_target_independently() {
let storage = fresh_storage();
storage
.save_edges(&[crate::model::Edge::builder(
"missing_src",
"missing_tgt",
EdgeType::Calls,
"demo",
)
.build()])
.expect("save_edges");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
2,
"expected two DQ-004 violations (source + target), got {violations:?}"
);
assert!(violations.iter().all(|v| v.rule == "DQ-004"));
let messages: Vec<&str> = violations.iter().map(|v| v.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("missing_src") && m.contains("source")),
"should report orphan source: {messages:?}"
);
assert!(
messages
.iter()
.any(|m| m.contains("missing_tgt") && m.contains("target")),
"should report orphan target: {messages:?}"
);
}
#[test]
fn test_dq005_clean_when_projects_isolated() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_project(&sample_project("beta", "beta"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("a1", "alpha", "main", "alpha.main")],
NodeLabel::Function,
)
.expect("save_nodes alpha");
storage
.save_nodes(
&[sample_function("b1", "beta", "main", "beta.main")],
NodeLabel::Function,
)
.expect("save_nodes beta");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations.is_empty(),
"expected no DQ-005 violations for isolated projects, got {violations:?}"
);
}
#[test]
fn test_dq005_detects_isolation_violation_for_unknown_project() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("a1", "alpha", "main", "alpha.main")],
NodeLabel::Function,
)
.expect("save_nodes alpha");
storage
.save_nodes(
&[sample_function("g1", "ghost", "main", "ghost.main")],
NodeLabel::Function,
)
.expect("save_nodes ghost");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert_eq!(
violations.len(),
1,
"expected exactly one DQ-005 violation, got {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-005");
assert!(
violations[0].message.contains("Function"),
"violation should mention the table: {}",
violations[0].message
);
assert!(
violations[0].message.contains("total 2"),
"total should be 2 (alpha + ghost): {}",
violations[0].message
);
assert!(
violations[0].message.contains("per-project sum 1"),
"per-project sum should be 1 (only alpha): {}",
violations[0].message
);
}
#[test]
fn test_run_all_includes_dq005_violations() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("g1", "ghost", "main", "ghost.main")],
NodeLabel::Function,
)
.expect("save_nodes ghost");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(
report.count_for_rule("DQ-005") >= 1,
"run_all should aggregate DQ-005 violations, got {:?}",
report.violations
);
}
#[test]
fn test_dq006_detects_empty_hash() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_file("f1", "demo", "/a.rs", "sha256:abc"),
sample_file("f2", "demo", "/b.rs", ""),
],
NodeLabel::File,
)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_hash_integrity()
.expect("check_hash_integrity");
assert_eq!(
violations.len(),
1,
"expected one DQ-006 violation, got {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-006");
assert!(violations[0].message.contains("f2"));
assert_eq!(violations[0].project.as_deref(), Some("demo"));
}
#[test]
fn test_dq006_clean_when_all_hashes_present() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_file("f1", "demo", "/a.rs", "sha256:abc"),
sample_file("f2", "demo", "/b.rs", "sha256:def"),
],
NodeLabel::File,
)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_hash_integrity()
.expect("check_hash_integrity");
assert!(
violations.is_empty(),
"expected no DQ-006 violations, got {violations:?}"
);
}
#[test]
fn test_quality_report_is_clean() {
let report = QualityReport::default();
assert!(report.is_clean());
let report = QualityReport {
violations: vec![QualityViolation {
rule: "DQ-002",
message: "dup".into(),
project: None,
}],
};
assert!(!report.is_clean());
}
#[test]
fn test_quality_report_count_for_rule() {
let report = QualityReport {
violations: vec![
QualityViolation {
rule: "DQ-002",
message: "dup1".into(),
project: None,
},
QualityViolation {
rule: "DQ-002",
message: "dup2".into(),
project: None,
},
QualityViolation {
rule: "DQ-006",
message: "empty hash".into(),
project: None,
},
],
};
assert_eq!(report.count_for_rule("DQ-002"), 2);
assert_eq!(report.count_for_rule("DQ-006"), 1);
assert_eq!(report.count_for_rule("DQ-004"), 0);
}
#[test]
fn test_run_all_clean_on_fresh_repo() {
let storage = fresh_storage();
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(report.is_clean(), "fresh repo should have no violations");
}
#[test]
fn test_run_all_aggregates_violations_from_all_checks() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "other", "demo.main"),
],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_nodes(
&[sample_file("file_1", "demo", "/a.rs", "")],
NodeLabel::File,
)
.expect("save_nodes file");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(!report.is_clean());
assert!(report.count_for_rule("DQ-002") >= 1);
assert!(report.count_for_rule("DQ-006") >= 1);
}
#[test]
fn escape_cypher_escapes_backslash_and_single_quote() {
assert_eq!(escape_cypher("plain"), "plain");
assert_eq!(escape_cypher("it's"), "it\\'s");
assert_eq!(escape_cypher("path\\to"), "path\\\\to");
assert_eq!(escape_cypher(""), "");
}
#[test]
fn test_dq002_multiple_duplicate_groups_generate_multiple_violations() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "alt", "demo.main"),
sample_function("f3", "demo", "helper", "demo.helper"),
sample_function("f4", "demo", "extra", "demo.helper"),
],
NodeLabel::Function,
)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert_eq!(
violations.len(),
2,
"expected two DQ-002 violations for two duplicate groups, got {violations:?}"
);
for v in &violations {
assert_eq!(v.rule, "DQ-002");
assert!(v.project.as_deref() == Some("demo"));
assert!(
v.message.contains("demo.main") || v.message.contains("demo.helper"),
"message should mention a duplicate FQN: {}",
v.message
);
assert!(
v.message.contains("2 nodes"),
"message should report 2 duplicate nodes: {}",
v.message
);
}
}
#[test]
fn test_dq002_file_nodes_handled_without_error() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_file("file1", "demo", "/a.rs", "hash1"),
sample_file("file2", "demo", "/b.rs", "hash2"),
],
NodeLabel::File,
)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert!(
violations.is_empty(),
"File nodes without qualifiedName should not produce DQ-002 violations: {violations:?}"
);
}
#[test]
fn test_dq004_only_source_orphan_produces_single_violation() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f2", "demo", "helper", "demo.helper")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_edges(&[crate::model::Edge::builder(
"missing_src",
"f2",
EdgeType::Calls,
"demo",
)
.build()])
.expect("save_edges");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
1,
"expected one DQ-004 violation for orphan source, got {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-004");
assert!(
violations[0].message.contains("missing_src")
&& violations[0].message.contains("source"),
"violation should mention orphan source: {}",
violations[0].message
);
}
#[test]
fn test_dq004_only_target_orphan_produces_single_violation() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_edges(&[crate::model::Edge::builder(
"f1",
"missing_target",
EdgeType::Calls,
"demo",
)
.build()])
.expect("save_edges");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
1,
"expected one DQ-004 violation for orphan target, got {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-004");
assert!(
violations[0].message.contains("missing_target")
&& violations[0].message.contains("target"),
"violation should mention orphan target: {}",
violations[0].message
);
}
#[test]
fn test_dq005_violation_in_file_table() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[sample_file("file1", "alpha", "/a.rs", "hash1")],
NodeLabel::File,
)
.expect("save_nodes alpha file");
storage
.save_nodes(
&[sample_file("file2", "ghost", "/b.rs", "hash2")],
NodeLabel::File,
)
.expect("save_nodes ghost file");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
let file_violations: Vec<_> = violations
.iter()
.filter(|v| v.message.contains("File"))
.collect();
assert_eq!(
file_violations.len(),
1,
"expected one DQ-005 violation for File table, got {violations:?}"
);
assert_eq!(file_violations[0].rule, "DQ-005");
assert!(
file_violations[0].message.contains("total 2"),
"total should be 2: {}",
file_violations[0].message
);
assert!(
file_violations[0].message.contains("per-project sum 1"),
"per-project sum should be 1: {}",
file_violations[0].message
);
}
#[test]
fn test_dq006_multiple_empty_hash_files() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_file("f1", "demo", "/a.rs", ""),
sample_file("f2", "demo", "/b.rs", ""),
sample_file("f3", "demo", "/c.rs", "sha256:valid"),
],
NodeLabel::File,
)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_hash_integrity()
.expect("check_hash_integrity");
assert_eq!(
violations.len(),
2,
"expected two DQ-006 violations, got {violations:?}"
);
for v in &violations {
assert_eq!(v.rule, "DQ-006");
assert_eq!(v.project.as_deref(), Some("demo"));
assert!(
v.message.contains("empty hash"),
"message should mention empty hash: {}",
v.message
);
}
let ids: Vec<&str> = violations
.iter()
.map(|v| v.message.split('\'').nth(1).unwrap_or(""))
.collect();
assert!(ids.contains(&"f1"), "should report f1: {ids:?}");
assert!(ids.contains(&"f2"), "should report f2: {ids:?}");
}
#[test]
fn test_run_all_with_all_four_violation_types() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[
sample_function("f1", "alpha", "main", "alpha.main"),
sample_function("f2", "alpha", "alt", "alpha.main"),
],
NodeLabel::Function,
)
.expect("save_nodes dup fqn");
storage
.save_edges(&[crate::model::Edge::builder(
"ghost_src",
"ghost_tgt",
EdgeType::Calls,
"alpha",
)
.build()])
.expect("save_edges orphan");
storage
.save_nodes(
&[sample_function("g1", "ghost", "main", "ghost.main")],
NodeLabel::Function,
)
.expect("save_nodes ghost");
storage
.save_nodes(
&[sample_file("file1", "alpha", "/a.rs", "")],
NodeLabel::File,
)
.expect("save_nodes empty hash file");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(!report.is_clean());
assert!(
report.count_for_rule("DQ-002") >= 1,
"should have DQ-002 violations: {:?}",
report.violations
);
assert!(
report.count_for_rule("DQ-004") >= 1,
"should have DQ-004 violations: {:?}",
report.violations
);
assert!(
report.count_for_rule("DQ-005") >= 1,
"should have DQ-005 violations: {:?}",
report.violations
);
assert!(
report.count_for_rule("DQ-006") >= 1,
"should have DQ-006 violations: {:?}",
report.violations
);
}
#[test]
fn test_dq005_multiple_projects_isolated_correctly() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project alpha");
storage
.save_project(&sample_project("beta", "beta"))
.expect("save_project beta");
storage
.save_project(&sample_project("gamma", "gamma"))
.expect("save_project gamma");
storage
.save_nodes(
&[sample_function("a1", "alpha", "main", "alpha.main")],
NodeLabel::Function,
)
.expect("save_nodes alpha");
storage
.save_nodes(
&[
sample_function("b1", "beta", "main", "beta.main"),
sample_function("b2", "beta", "helper", "beta.helper"),
],
NodeLabel::Function,
)
.expect("save_nodes beta");
storage
.save_nodes(
&[sample_function("g1", "gamma", "main", "gamma.main")],
NodeLabel::Function,
)
.expect("save_nodes gamma");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations.is_empty(),
"three isolated projects should have no DQ-005 violations: {violations:?}"
);
}
#[test]
fn test_dq002_skips_table_on_query_error() {
let storage = fresh_storage();
storage
.save_nodes(
&[
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "other", "demo.main"),
],
NodeLabel::Function,
)
.expect("save_nodes");
storage.execute("DROP TABLE Variable;").expect("drop table");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert_eq!(
violations.len(),
1,
"should still detect DQ-002 in Function table"
);
assert_eq!(violations[0].rule, "DQ-002");
}
#[test]
fn test_dq004_skips_table_on_query_error() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_edges(&[crate::model::Edge::builder(
"f1",
"missing_target",
EdgeType::Calls,
"demo",
)
.build()])
.expect("save_edges");
storage.execute("DROP TABLE Variable;").expect("drop table");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
1,
"should still detect DQ-004 orphan target"
);
assert_eq!(violations[0].rule, "DQ-004");
assert!(violations[0].message.contains("missing_target"));
}
#[test]
fn test_dq005_skips_table_on_query_error() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("a1", "alpha", "main", "alpha.main")],
NodeLabel::Function,
)
.expect("save_nodes alpha");
storage
.save_nodes(
&[sample_function("g1", "ghost", "main", "ghost.main")],
NodeLabel::Function,
)
.expect("save_nodes ghost");
storage.execute("DROP TABLE Variable;").expect("drop table");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations
.iter()
.any(|v| v.rule == "DQ-005" && v.message.contains("Function")),
"should still detect DQ-005 in Function table: {violations:?}"
);
}
#[test]
fn test_run_all_skips_edge_integrity_on_code_relation_drop() {
let storage = fresh_storage();
storage
.save_project(&sample_project("demo", "demo"))
.expect("save_project");
storage
.save_nodes(
&[
sample_function("f1", "demo", "main", "demo.main"),
sample_function("f2", "demo", "other", "demo.main"),
],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.execute("DROP TABLE CodeRelation;")
.expect("drop CodeRelation");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(
report.count_for_rule("DQ-002") > 0,
"DQ-002 should still run and detect duplicate FQN"
);
}
#[test]
fn test_run_all_skips_hash_integrity_on_file_table_drop() {
let storage = fresh_storage();
storage
.save_project(&sample_project("demo", "demo"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage.execute("DROP TABLE File;").expect("drop File");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(report.is_clean(), "DQ-006 skipped, other checks clean");
}
#[test]
fn test_run_all_skips_project_isolation_on_project_table_drop() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.execute("DROP TABLE Project;")
.expect("drop Project");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert_eq!(
report.count_for_rule("DQ-005"),
0,
"DQ-005 should be skipped when Project table is dropped"
);
}
#[test]
fn test_dq002_skips_node_with_empty_qualified_name() {
let storage = fresh_storage();
storage
.save_project(&sample_project("demo", "demo"))
.expect("save_project");
let empty_qn_func = crate::model::Node::builder(NodeLabel::Function, "empty_qn", "")
.id("f1")
.project("demo")
.file_path("/src/empty.rs")
.start_line(1)
.end_line(5)
.build();
storage
.save_nodes(&[empty_qn_func], NodeLabel::Function)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert!(
violations.is_empty(),
"node with empty qn should be skipped, got {violations:?}"
);
}
#[test]
fn test_dq002_skips_node_with_empty_project() {
let storage = fresh_storage();
let empty_project_func =
crate::model::Node::builder(NodeLabel::Function, "no_proj", "demo.no_proj")
.id("f1")
.project("")
.file_path("/src/no_proj.rs")
.start_line(1)
.end_line(5)
.build();
storage
.save_nodes(&[empty_project_func], NodeLabel::Function)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert!(
violations.is_empty(),
"node with empty project should be skipped, got {violations:?}"
);
}
#[test]
fn test_dq005_no_projects_returns_empty() {
let storage = fresh_storage();
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations.is_empty(),
"no projects and no nodes → no violations: {violations:?}"
);
}
#[test]
fn test_dq005_violation_when_nodes_exist_but_no_projects() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations
.iter()
.any(|v| v.rule == "DQ-005" && v.message.contains("Function")),
"should detect DQ-005 for Function table: {violations:?}"
);
}
#[test]
fn test_dq006_detects_null_hash() {
let storage = fresh_storage();
let no_hash_file = crate::model::Node::builder(NodeLabel::File, "/c.rs", "/c.rs")
.id("f1")
.project("demo")
.file_path("/c.rs")
.language(Language::Rust)
.properties(serde_json::json!({"lineCount": 50}))
.build();
storage
.save_nodes(&[no_hash_file], NodeLabel::File)
.expect("save_nodes");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_hash_integrity()
.expect("check_hash_integrity");
assert_eq!(
violations.len(),
1,
"null hash should be detected: {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-006");
assert!(violations[0].message.contains("f1"));
}
#[test]
fn test_run_all_skips_fqn_uniqueness_on_function_table_drop() {
let storage = fresh_storage();
storage
.save_project(&sample_project("demo", "demo"))
.expect("save_project");
storage
.save_nodes(
&[sample_file("file1", "demo", "/a.rs", "")],
NodeLabel::File,
)
.expect("save_nodes file");
storage
.execute("DROP TABLE Function;")
.expect("drop Function");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert_eq!(
report.count_for_rule("DQ-002"),
0,
"DQ-002 should be skipped when Function table is dropped"
);
assert!(
report.count_for_rule("DQ-006") >= 1,
"DQ-006 should still detect empty hash"
);
}
#[test]
fn check_edge_integrity_returns_err_when_code_relation_dropped() {
let storage = fresh_storage();
storage
.execute("DROP TABLE CodeRelation;")
.expect("drop CodeRelation");
let checker = QualityChecker::new(&*storage);
assert!(
checker.check_edge_integrity().is_err(),
"check_edge_integrity should return Err when CodeRelation table is dropped"
);
}
#[test]
fn check_hash_integrity_returns_err_when_file_table_dropped() {
let storage = fresh_storage();
storage.execute("DROP TABLE File;").expect("drop File");
let checker = QualityChecker::new(&*storage);
assert!(
checker.check_hash_integrity().is_err(),
"check_hash_integrity should return Err when File table is dropped"
);
}
#[test]
fn check_project_isolation_returns_err_when_project_table_dropped() {
let storage = fresh_storage();
storage
.execute("DROP TABLE Project;")
.expect("drop Project");
let checker = QualityChecker::new(&*storage);
assert!(
checker.check_project_isolation().is_err(),
"check_project_isolation should return Err when Project table is dropped"
);
}
#[test]
fn quality_report_count_for_rule_nonexistent_returns_zero() {
let report = QualityReport {
violations: vec![QualityViolation {
rule: "DQ-002",
message: "test".into(),
project: None,
}],
};
assert_eq!(
report.count_for_rule("DQ-999"),
0,
"non-existent rule should return 0"
);
}
#[test]
fn test_dq002_duplicates_in_method_table_only() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes function");
let method1 = crate::model::Node::builder(NodeLabel::Method, "process", "demo.process")
.id("m1")
.project("demo")
.file_path("/src/m.rs")
.start_line(1)
.end_line(5)
.build();
let method2 = crate::model::Node::builder(NodeLabel::Method, "handle", "demo.process")
.id("m2")
.project("demo")
.file_path("/src/m.rs")
.start_line(10)
.end_line(15)
.build();
storage
.save_nodes(&[method1, method2], NodeLabel::Method)
.expect("save_nodes method");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert_eq!(
violations.len(),
1,
"should detect duplicate FQN in Method table: {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-002");
assert!(violations[0].message.contains("demo.process"));
}
#[test]
fn test_dq002_duplicates_across_function_and_method_tables() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes function");
let method = crate::model::Node::builder(NodeLabel::Method, "main", "demo.main")
.id("m1")
.project("demo")
.file_path("/src/m.rs")
.start_line(1)
.end_line(5)
.build();
storage
.save_nodes(&[method], NodeLabel::Method)
.expect("save_nodes method");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_fqn_uniqueness()
.expect("check_fqn_uniqueness");
assert_eq!(
violations.len(),
1,
"should detect duplicate FQN across Function and Method: {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-002");
assert!(violations[0].message.contains("2 nodes"));
}
#[test]
fn test_dq004_multiple_edges_sorted_deterministically() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.save_edges(&[
crate::model::Edge::builder("f1", "zebra_missing", EdgeType::Calls, "demo").build(),
crate::model::Edge::builder("f1", "alpha_missing", EdgeType::Calls, "demo").build(),
crate::model::Edge::builder("f1", "mid_missing", EdgeType::Calls, "demo").build(),
])
.expect("save_edges");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
3,
"should detect three orphan targets: {violations:?}"
);
let targets: Vec<&str> = violations
.iter()
.map(|v| v.message.split('\'').nth(1).unwrap_or(""))
.collect();
assert_eq!(
targets,
vec!["alpha_missing", "mid_missing", "zebra_missing"],
"violations should be sorted by message: {targets:?}"
);
}
#[test]
fn test_dq005_violations_for_multiple_tables_when_no_projects() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes function");
storage
.save_nodes(
&[sample_file("file1", "demo", "/a.rs", "hash1")],
NodeLabel::File,
)
.expect("save_nodes file");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations
.iter()
.any(|v| v.rule == "DQ-005" && v.message.contains("Function")),
"should have DQ-005 for Function table: {violations:?}"
);
assert!(
violations
.iter()
.any(|v| v.rule == "DQ-005" && v.message.contains("File")),
"should have DQ-005 for File table: {violations:?}"
);
}
#[test]
fn test_dq005_per_project_count_sums_across_multiple_tables() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("a1", "alpha", "main", "alpha.main")],
NodeLabel::Function,
)
.expect("save_nodes function");
storage
.save_nodes(
&[sample_file("file1", "alpha", "/a.rs", "hash1")],
NodeLabel::File,
)
.expect("save_nodes file");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_project_isolation()
.expect("check_project_isolation");
assert!(
violations.is_empty(),
"single project with all nodes should have no DQ-005: {violations:?}"
);
}
#[test]
fn test_run_all_with_only_dq005_asserts_other_rules_clean() {
let storage = fresh_storage();
storage
.save_project(&sample_project("alpha", "alpha"))
.expect("save_project");
storage
.save_nodes(
&[sample_function("g1", "ghost", "main", "ghost.main")],
NodeLabel::Function,
)
.expect("save_nodes ghost");
let checker = QualityChecker::new(&*storage);
let report = checker.run_all().expect("run_all");
assert!(
report.count_for_rule("DQ-005") >= 1,
"should have DQ-005 violations: {:?}",
report.violations
);
assert_eq!(
report.count_for_rule("DQ-002"),
0,
"should have no DQ-002: {:?}",
report.violations
);
assert_eq!(
report.count_for_rule("DQ-004"),
0,
"should have no DQ-004: {:?}",
report.violations
);
assert_eq!(
report.count_for_rule("DQ-006"),
0,
"should have no DQ-006: {:?}",
report.violations
);
}
#[test]
fn escape_cypher_combined_backslash_and_single_quote() {
assert_eq!(escape_cypher("it's\\path"), "it\\'s\\\\path");
assert_eq!(escape_cypher("\\'"), "\\\\\\'");
}
#[test]
fn test_dq006_handles_null_project_in_file_node() {
let storage = fresh_storage();
storage
.execute(
"CREATE (:File {id: 'f1', project: NULL, name: 'a.rs', \
filePath: '/a.rs', language: 'rust', hash: '', lineCount: 0});",
)
.expect("insert file with null project");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_hash_integrity()
.expect("check_hash_integrity");
assert_eq!(violations.len(), 1, "should detect DQ-006: {violations:?}");
assert_eq!(violations[0].rule, "DQ-006");
assert_eq!(
violations[0].project.as_deref(),
Some(""),
"null project should map to empty string"
);
}
#[test]
fn test_dq004_handles_null_source_in_edge() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.execute(
"CREATE (:CodeRelation {id: 'e1', source: NULL, target: 'f1', \
type: 'CALLS', confidence: 1.0, confidenceTier: 'HIGH', \
reason: 'test', startLine: 1, project: 'demo'});",
)
.expect("insert edge with null source");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
1,
"should detect one DQ-004 for null source: {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-004");
assert!(
violations[0].message.contains("source"),
"violation should mention source: {}",
violations[0].message
);
}
#[test]
fn test_dq004_handles_null_project_in_edge() {
let storage = fresh_storage();
storage
.save_nodes(
&[sample_function("f1", "demo", "main", "demo.main")],
NodeLabel::Function,
)
.expect("save_nodes");
storage
.execute(
"CREATE (:CodeRelation {id: 'e1', source: 'f1', target: 'ghost', \
type: 'CALLS', confidence: 1.0, confidenceTier: 'HIGH', \
reason: 'test', startLine: 1, project: NULL});",
)
.expect("insert edge with null project");
let checker = QualityChecker::new(&*storage);
let violations = checker
.check_edge_integrity()
.expect("check_edge_integrity");
assert_eq!(
violations.len(),
1,
"should detect one DQ-004 for orphan target: {violations:?}"
);
assert_eq!(violations[0].rule, "DQ-004");
assert_eq!(
violations[0].project.as_deref(),
Some(""),
"null project should map to empty string"
);
}
}