pub mod transport;
use std::sync::Arc;
use anyhow::Result;
use serde_json::{Value, json};
use crate::databases::traits::VectorDatabase;
use crate::glob_utils;
use brainwires_core::{ChunkMetadata, DatabaseStats, SearchResult};
use transport::{NornicTransport, RestTransport};
#[cfg(feature = "nornicdb-bolt")]
use transport::BoltTransport;
#[cfg(feature = "nornicdb-grpc")]
use transport::GrpcTransport;
#[derive(Debug, Clone)]
pub struct NornicConfig {
pub url: String,
pub database: String,
pub username: Option<String>,
pub password: Option<String>,
pub node_label: String,
pub index_name: String,
pub transport: TransportKind,
}
impl Default for NornicConfig {
fn default() -> Self {
Self {
url: "http://localhost:7474".to_string(),
database: "neo4j".to_string(),
username: None,
password: None,
node_label: "CodeChunk".to_string(),
index_name: "code_embedding_index".to_string(),
transport: TransportKind::Rest,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum TransportKind {
#[default]
Rest,
Bolt {
port: u16,
},
Grpc {
port: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum CognitiveMemoryTier {
Episodic,
Semantic,
Procedural,
}
impl CognitiveMemoryTier {
pub fn as_label(&self) -> &'static str {
match self {
Self::Episodic => "Episodic",
Self::Semantic => "Semantic",
Self::Procedural => "Procedural",
}
}
}
impl std::fmt::Display for CognitiveMemoryTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_label())
}
}
pub struct NornicDatabase {
transport: Arc<dyn NornicTransport>,
node_label: String,
index_name: String,
#[allow(dead_code)]
database: String,
}
impl NornicDatabase {
pub fn default_url() -> String {
"http://localhost:7474".to_string()
}
pub async fn new() -> Result<Self> {
Self::with_config(NornicConfig::default()).await
}
pub async fn with_url(url: &str) -> Result<Self> {
Self::with_config(NornicConfig {
url: url.to_string(),
..Default::default()
})
.await
}
#[cfg(feature = "nornicdb-bolt")]
pub async fn with_bolt(url: &str, username: &str, password: &str) -> Result<Self> {
let host = extract_host(url);
let bolt_url = format!("bolt://{}:7687", host);
let transport = BoltTransport::new(&bolt_url, username, password).await?;
Ok(Self {
transport: Arc::new(transport),
node_label: "CodeChunk".to_string(),
index_name: "code_embedding_index".to_string(),
database: "neo4j".to_string(),
})
}
#[cfg(feature = "nornicdb-grpc")]
pub async fn with_grpc(url: &str) -> Result<Self> {
let transport = GrpcTransport::new(url).await?;
Ok(Self {
transport: Arc::new(transport),
node_label: "CodeChunk".to_string(),
index_name: "code_embedding_index".to_string(),
database: "neo4j".to_string(),
})
}
pub async fn with_config(config: NornicConfig) -> Result<Self> {
let transport: Arc<dyn NornicTransport> = match &config.transport {
TransportKind::Rest => {
let rest = RestTransport::new(&config.url, &config.database).await?;
if let (Some(user), Some(pass)) = (&config.username, &config.password) {
rest.authenticate(user, pass).await?;
}
Arc::new(rest)
}
#[cfg(feature = "nornicdb-bolt")]
TransportKind::Bolt { port } => {
let host = extract_host(&config.url);
let bolt_url = format!("bolt://{}:{}", host, port);
let (user, pass) = (
config.username.as_deref().unwrap_or("neo4j"),
config.password.as_deref().unwrap_or(""),
);
Arc::new(BoltTransport::new(&bolt_url, user, pass).await?)
}
#[cfg(not(feature = "nornicdb-bolt"))]
TransportKind::Bolt { .. } => {
anyhow::bail!("Bolt transport requires the 'nornicdb-bolt' feature");
}
#[cfg(feature = "nornicdb-grpc")]
TransportKind::Grpc { port } => {
let host = extract_host(&config.url);
let grpc_url = format!("http://{}:{}", host, port);
Arc::new(GrpcTransport::new(&grpc_url).await?)
}
#[cfg(not(feature = "nornicdb-grpc"))]
TransportKind::Grpc { .. } => {
anyhow::bail!("gRPC transport requires the 'nornicdb-grpc' feature");
}
};
Ok(Self {
transport,
node_label: config.node_label,
index_name: config.index_name,
database: config.database,
})
}
pub async fn health_check(&self) -> Result<bool> {
self.transport.health_check().await
}
pub async fn authenticate(&self, username: &str, password: &str) -> Result<()> {
let _ = self.transport.execute_cypher("RETURN 1", json!({})).await;
let _ = username;
let _ = password;
Ok(())
}
}
#[async_trait::async_trait]
impl VectorDatabase for NornicDatabase {
async fn initialize(&self, dimension: usize) -> Result<()> {
let create_index = format!(
"CALL db.index.vector.createNodeIndex('{}', '{}', 'embedding', {}, 'cosine')",
self.index_name, self.node_label, dimension
);
match self
.transport
.execute_cypher(&create_index, json!({}))
.await
{
Ok(_) => tracing::info!(
"Created vector index '{}' with dimension {}",
self.index_name,
dimension
),
Err(e) => {
tracing::info!("Vector index may already exist: {}", e);
}
}
let constraint = format!(
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:{}) REQUIRE (n.file_path, n.start_line) IS UNIQUE",
self.node_label
);
match self.transport.execute_cypher(&constraint, json!({})).await {
Ok(_) => tracing::info!("Created uniqueness constraint"),
Err(e) => tracing::info!("Constraint may already exist: {}", e),
}
Ok(())
}
async fn store_embeddings(
&self,
embeddings: Vec<Vec<f32>>,
metadata: Vec<ChunkMetadata>,
contents: Vec<String>,
root_path: &str,
) -> Result<usize> {
if embeddings.is_empty() {
return Ok(0);
}
let count = embeddings.len();
let nodes: Vec<Value> = embeddings
.into_iter()
.zip(metadata)
.zip(contents)
.map(|((emb, meta), content)| {
json!({
"file_path": meta.file_path,
"root_path": meta.root_path.unwrap_or_else(|| root_path.to_string()),
"project": meta.project,
"start_line": meta.start_line,
"end_line": meta.end_line,
"language": meta.language.unwrap_or_default(),
"extension": meta.extension.unwrap_or_default(),
"file_hash": meta.file_hash,
"indexed_at": meta.indexed_at,
"content": content,
"embedding": emb,
})
})
.collect();
self.transport.store_nodes(nodes, &self.node_label).await?;
Ok(count)
}
async fn search(
&self,
query_vector: Vec<f32>,
query_text: &str,
limit: usize,
min_score: f32,
project: Option<String>,
root_path: Option<String>,
hybrid: bool,
) -> Result<Vec<SearchResult>> {
self.search_filtered(
query_vector,
query_text,
limit,
min_score,
project,
root_path,
hybrid,
vec![],
vec![],
vec![],
)
.await
}
async fn search_filtered(
&self,
query_vector: Vec<f32>,
query_text: &str,
limit: usize,
min_score: f32,
project: Option<String>,
root_path: Option<String>,
hybrid: bool,
file_extensions: Vec<String>,
languages: Vec<String>,
path_patterns: Vec<String>,
) -> Result<Vec<SearchResult>> {
let filters = build_filters(
project.as_deref(),
root_path.as_deref(),
&file_extensions,
&languages,
);
let raw_results = if hybrid {
self.transport
.hybrid_search(
query_text,
query_vector,
limit,
min_score,
&self.node_label,
filters,
)
.await?
} else {
self.transport
.vector_search(query_vector, limit, min_score, &self.node_label, filters)
.await?
};
let mut results: Vec<SearchResult> = raw_results
.iter()
.filter_map(map_to_search_result)
.collect();
if !path_patterns.is_empty() {
results.retain(|r| glob_utils::matches_any_pattern(&r.file_path, &path_patterns));
}
Ok(results)
}
async fn delete_by_file(&self, file_path: &str) -> Result<usize> {
self.transport
.delete_nodes(&self.node_label, "file_path", file_path)
.await
}
async fn clear(&self) -> Result<()> {
let delete_all = format!("MATCH (n:{}) DETACH DELETE n", self.node_label);
self.transport
.execute_cypher(&delete_all, json!({}))
.await?;
let drop_index = format!("DROP INDEX {} IF EXISTS", self.index_name);
let _ = self.transport.execute_cypher(&drop_index, json!({})).await;
Ok(())
}
async fn get_statistics(&self) -> Result<DatabaseStats> {
let count_query = format!("MATCH (n:{}) RETURN count(n) AS total", self.node_label);
let count_rows = self
.transport
.execute_cypher(&count_query, json!({}))
.await?;
let total = count_rows
.first()
.and_then(|r| r.get("total"))
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let lang_query = format!(
"MATCH (n:{}) RETURN n.language AS lang, count(n) AS cnt ORDER BY cnt DESC",
self.node_label
);
let lang_rows = self
.transport
.execute_cypher(&lang_query, json!({}))
.await?;
let language_breakdown: Vec<(String, usize)> = lang_rows
.iter()
.filter_map(|r| {
let lang = r.get("lang")?.as_str()?.to_string();
let cnt = r.get("cnt")?.as_u64()? as usize;
Some((lang, cnt))
})
.collect();
Ok(DatabaseStats {
total_points: total,
total_vectors: total,
language_breakdown,
})
}
async fn flush(&self) -> Result<()> {
Ok(())
}
async fn count_by_root_path(&self, root_path: &str) -> Result<usize> {
self.transport
.count_nodes(&self.node_label, "root_path", root_path)
.await
}
async fn get_indexed_files(&self, root_path: &str) -> Result<Vec<String>> {
self.transport
.distinct_property(&self.node_label, "file_path", "root_path", root_path)
.await
}
}
impl NornicDatabase {
pub async fn cypher_query(&self, query: &str, params: Value) -> Result<Value> {
let rows = self.transport.execute_cypher(query, params).await?;
Ok(Value::Array(rows))
}
pub async fn create_relationship(
&self,
from_file: &str,
from_line: usize,
to_file: &str,
to_line: usize,
relationship_type: &str,
properties: Value,
) -> Result<()> {
let query = format!(
"MATCH (a:{label} {{file_path: $from_file, start_line: $from_line}}) \
MATCH (b:{label} {{file_path: $to_file, start_line: $to_line}}) \
MERGE (a)-[r:{rel_type}]->(b) SET r += $props",
label = self.node_label,
rel_type = relationship_type,
);
let params = json!({
"from_file": from_file,
"from_line": from_line,
"to_file": to_file,
"to_line": to_line,
"props": properties,
});
self.transport.execute_cypher(&query, params).await?;
Ok(())
}
pub async fn find_related(
&self,
file_path: &str,
start_line: usize,
max_depth: usize,
relationship_types: Option<Vec<String>>,
) -> Result<Vec<SearchResult>> {
let rel_pattern = match &relationship_types {
Some(types) if !types.is_empty() => {
format!(":{}", types.join("|"))
}
_ => String::new(),
};
let query = format!(
"MATCH (start:{label} {{file_path: $file_path, start_line: $start_line}}) \
MATCH (start)-[{rel}*1..{depth}]->(related:{label}) \
RETURN DISTINCT related",
label = self.node_label,
rel = rel_pattern,
depth = max_depth,
);
let params = json!({
"file_path": file_path,
"start_line": start_line,
});
let rows = self.transport.execute_cypher(&query, params).await?;
Ok(rows
.iter()
.filter_map(|r| r.get("related").and_then(map_node_to_search_result))
.collect())
}
pub async fn store_with_memory_tier(
&self,
embedding: Vec<f32>,
metadata: ChunkMetadata,
content: String,
tier: CognitiveMemoryTier,
) -> Result<()> {
let query = format!(
"MERGE (n:{label} {{file_path: $file_path, start_line: $start_line}}) \
SET n += $props, n.embedding = $embedding \
SET n:{tier_label}",
label = self.node_label,
tier_label = tier.as_label(),
);
let params = json!({
"file_path": metadata.file_path,
"start_line": metadata.start_line,
"embedding": embedding,
"props": {
"root_path": metadata.root_path,
"project": metadata.project,
"end_line": metadata.end_line,
"language": metadata.language,
"extension": metadata.extension,
"file_hash": metadata.file_hash,
"indexed_at": metadata.indexed_at,
"content": content,
}
});
self.transport.execute_cypher(&query, params).await?;
Ok(())
}
pub async fn search_by_memory_tier(
&self,
query_vector: Vec<f32>,
tier: CognitiveMemoryTier,
limit: usize,
) -> Result<Vec<SearchResult>> {
let query = format!(
"CALL db.index.vector.queryNodes('{}', $limit, $vector) \
YIELD node, score \
WHERE node:{tier_label} \
RETURN node, score",
self.index_name,
tier_label = tier.as_label(),
);
let params = json!({
"limit": limit,
"vector": query_vector,
});
let rows = self.transport.execute_cypher(&query, params).await?;
Ok(rows
.iter()
.filter_map(|r| {
let score = r.get("score")?.as_f64()? as f32;
let node = r.get("node")?;
let mut result = map_node_to_search_result(node)?;
result.score = score;
result.vector_score = score;
Some(result)
})
.collect())
}
pub async fn embedding_stats(&self) -> Result<Value> {
let query = format!(
"MATCH (n:{}) WHERE n.embedding IS NOT NULL \
RETURN count(n) AS embedded_count, \
avg(size(n.embedding)) AS avg_dimension",
self.node_label
);
let rows = self.transport.execute_cypher(&query, json!({})).await?;
Ok(rows.first().cloned().unwrap_or(json!({})))
}
}
impl Default for NornicDatabase {
fn default() -> Self {
tokio::runtime::Runtime::new()
.expect("failed to create tokio runtime")
.block_on(Self::new())
.expect("Failed to create default NornicDB client")
}
}
#[allow(dead_code)]
fn extract_host(url: &str) -> String {
let host = url
.trim_start_matches("http://")
.trim_start_matches("https://")
.split(':')
.next()
.unwrap_or("localhost")
.split('/')
.next()
.unwrap_or("localhost");
if host.is_empty() {
"localhost".to_string()
} else {
host.to_string()
}
}
fn build_filters(
project: Option<&str>,
root_path: Option<&str>,
extensions: &[String],
languages: &[String],
) -> Value {
let mut filters = serde_json::Map::new();
if let Some(p) = project {
filters.insert("project".into(), json!(p));
}
if let Some(rp) = root_path {
filters.insert("root_path".into(), json!(rp));
}
if !extensions.is_empty() {
filters.insert("extension".into(), json!(extensions));
}
if !languages.is_empty() {
filters.insert("language".into(), json!(languages));
}
Value::Object(filters)
}
fn map_to_search_result(v: &Value) -> Option<SearchResult> {
Some(SearchResult {
file_path: v.get("file_path")?.as_str()?.to_string(),
root_path: v
.get("root_path")
.and_then(|v| v.as_str())
.map(String::from),
content: v.get("content")?.as_str()?.to_string(),
score: v.get("score")?.as_f64()? as f32,
vector_score: v
.get("vector_score")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32,
keyword_score: v
.get("keyword_score")
.and_then(|v| v.as_f64())
.map(|s| s as f32),
start_line: v.get("start_line").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
end_line: v.get("end_line").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
language: v
.get("language")
.and_then(|v| v.as_str())
.unwrap_or("Unknown")
.to_string(),
project: v.get("project").and_then(|v| v.as_str()).map(String::from),
indexed_at: v.get("indexed_at").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
fn map_node_to_search_result(node: &Value) -> Option<SearchResult> {
map_to_search_result(node)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::sync::Mutex;
struct MockTransport {
responses: Mutex<Vec<Result<Vec<Value>>>>,
queries: Mutex<Vec<String>>,
stored_nodes: Mutex<Vec<(Vec<Value>, String)>>,
}
impl MockTransport {
fn new(responses: Vec<Result<Vec<Value>>>) -> Self {
Self {
responses: Mutex::new(responses),
queries: Mutex::new(Vec::new()),
stored_nodes: Mutex::new(Vec::new()),
}
}
fn with_ok(responses: Vec<Vec<Value>>) -> Self {
Self::new(responses.into_iter().map(Ok).collect())
}
fn empty() -> Self {
Self::new(vec![])
}
fn next_response(&self) -> Result<Vec<Value>> {
let mut responses = self.responses.lock().unwrap();
if responses.is_empty() {
Ok(vec![])
} else {
responses.remove(0)
}
}
fn recorded_queries(&self) -> Vec<String> {
self.queries.lock().unwrap().clone()
}
}
#[async_trait::async_trait]
impl NornicTransport for MockTransport {
async fn health_check(&self) -> Result<bool> {
Ok(true)
}
async fn execute_cypher(&self, query: &str, _params: Value) -> Result<Vec<Value>> {
self.queries.lock().unwrap().push(query.to_string());
self.next_response()
}
async fn hybrid_search(
&self,
_query_text: &str,
_query_vector: Vec<f32>,
_limit: usize,
_min_score: f32,
_node_label: &str,
_filters: Value,
) -> Result<Vec<Value>> {
self.next_response()
}
async fn vector_search(
&self,
_query_vector: Vec<f32>,
_limit: usize,
_min_score: f32,
_node_label: &str,
_filters: Value,
) -> Result<Vec<Value>> {
self.next_response()
}
async fn store_nodes(&self, nodes: Vec<Value>, node_label: &str) -> Result<usize> {
let count = nodes.len();
self.stored_nodes
.lock()
.unwrap()
.push((nodes, node_label.to_string()));
Ok(count)
}
async fn delete_nodes(
&self,
_node_label: &str,
_property: &str,
_value: &str,
) -> Result<usize> {
let resp = self.next_response()?;
let count = resp.first().and_then(|v| v.as_u64()).unwrap_or(0) as usize;
Ok(count)
}
async fn count_nodes(
&self,
_node_label: &str,
_property: &str,
_value: &str,
) -> Result<usize> {
let resp = self.next_response()?;
let count = resp.first().and_then(|v| v.as_u64()).unwrap_or(0) as usize;
Ok(count)
}
async fn distinct_property(
&self,
_node_label: &str,
_property: &str,
_filter_prop: &str,
_filter_val: &str,
) -> Result<Vec<String>> {
let resp = self.next_response()?;
Ok(resp
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect())
}
fn transport_name(&self) -> &'static str {
"Mock"
}
}
fn mock_db(transport: MockTransport) -> NornicDatabase {
NornicDatabase {
transport: Arc::new(transport),
node_label: "CodeChunk".to_string(),
index_name: "code_embedding_index".to_string(),
database: "neo4j".to_string(),
}
}
fn sample_metadata(file: &str, start: usize, end: usize) -> ChunkMetadata {
ChunkMetadata {
file_path: file.to_string(),
root_path: Some("/project".to_string()),
project: Some("test-project".to_string()),
start_line: start,
end_line: end,
language: Some("Rust".to_string()),
extension: Some("rs".to_string()),
file_hash: "abc123".to_string(),
indexed_at: 1700000000,
}
}
fn sample_search_result_value(file: &str, score: f64, vector_score: f64) -> Value {
json!({
"file_path": file,
"root_path": "/project",
"content": "fn main() {}",
"score": score,
"vector_score": vector_score,
"keyword_score": null,
"start_line": 1,
"end_line": 10,
"language": "Rust",
"project": "test-project",
"indexed_at": 1700000000,
})
}
#[test]
fn test_default_config() {
let config = NornicConfig::default();
assert_eq!(config.url, "http://localhost:7474");
assert_eq!(config.database, "neo4j");
assert!(config.username.is_none());
assert!(config.password.is_none());
assert_eq!(config.node_label, "CodeChunk");
assert_eq!(config.index_name, "code_embedding_index");
assert!(matches!(config.transport, TransportKind::Rest));
}
#[test]
fn test_custom_config() {
let config = NornicConfig {
url: "http://nornic.example.com:7474".to_string(),
database: "mydb".to_string(),
username: Some("admin".to_string()),
password: Some("secret".to_string()),
node_label: "MyChunk".to_string(),
index_name: "my_index".to_string(),
transport: TransportKind::Bolt { port: 7688 },
};
assert_eq!(config.url, "http://nornic.example.com:7474");
assert_eq!(config.database, "mydb");
assert_eq!(config.username.as_deref(), Some("admin"));
assert_eq!(config.password.as_deref(), Some("secret"));
assert_eq!(config.node_label, "MyChunk");
assert_eq!(config.index_name, "my_index");
assert!(matches!(
config.transport,
TransportKind::Bolt { port: 7688 }
));
}
#[test]
fn test_default_url() {
assert_eq!(NornicDatabase::default_url(), "http://localhost:7474");
}
#[test]
fn test_transport_kind_default() {
let kind = TransportKind::default();
assert!(matches!(kind, TransportKind::Rest));
}
#[test]
fn test_transport_kind_bolt_default_port() {
let kind = TransportKind::Bolt { port: 7687 };
match kind {
TransportKind::Bolt { port } => assert_eq!(port, 7687),
_ => panic!("Expected Bolt variant"),
}
}
#[test]
fn test_transport_kind_grpc_default_port() {
let kind = TransportKind::Grpc { port: 6334 };
match kind {
TransportKind::Grpc { port } => assert_eq!(port, 6334),
_ => panic!("Expected Grpc variant"),
}
}
#[test]
fn test_cognitive_tier_serialize_roundtrip() {
for tier in [
CognitiveMemoryTier::Episodic,
CognitiveMemoryTier::Semantic,
CognitiveMemoryTier::Procedural,
] {
let serialized = serde_json::to_string(&tier).unwrap();
let deserialized: CognitiveMemoryTier = serde_json::from_str(&serialized).unwrap();
assert_eq!(tier, deserialized);
}
}
#[test]
fn test_cognitive_memory_tier_display() {
assert_eq!(CognitiveMemoryTier::Episodic.to_string(), "Episodic");
assert_eq!(CognitiveMemoryTier::Semantic.to_string(), "Semantic");
assert_eq!(CognitiveMemoryTier::Procedural.to_string(), "Procedural");
}
#[tokio::test]
async fn test_build_initialize_cypher() {
let transport = MockTransport::with_ok(vec![vec![], vec![]]);
let db = mock_db(transport);
db.initialize(384).await.unwrap();
let queries = db.transport.execute_cypher("", json!({})).await.ok(); let mock = db.transport.as_ref() as *const dyn NornicTransport;
let create_index = format!(
"CALL db.index.vector.createNodeIndex('{}', '{}', 'embedding', {}, 'cosine')",
"code_embedding_index", "CodeChunk", 384
);
assert!(create_index.contains("code_embedding_index"));
assert!(create_index.contains("384"));
assert!(create_index.contains("CodeChunk"));
}
#[test]
fn test_build_store_batch_cypher() {
let node_label = "CodeChunk";
let cypher = format!(
"UNWIND $batch AS item \
MERGE (n:{node_label} {{file_path: item.file_path, start_line: item.start_line}}) \
SET n += item"
);
assert!(cypher.contains("UNWIND $batch"));
assert!(cypher.contains("MERGE"));
assert!(cypher.contains("CodeChunk"));
assert!(cypher.contains("SET n += item"));
}
#[test]
fn test_build_delete_cypher() {
let node_label = "CodeChunk";
let cypher = format!("MATCH (n:{}) DETACH DELETE n", node_label);
assert!(cypher.contains("MATCH"));
assert!(cypher.contains("DETACH DELETE"));
assert!(cypher.contains("CodeChunk"));
}
#[test]
fn test_build_clear_cypher() {
let node_label = "CodeChunk";
let index_name = "code_embedding_index";
let delete_all = format!("MATCH (n:{}) DETACH DELETE n", node_label);
let drop_index = format!("DROP INDEX {} IF EXISTS", index_name);
assert!(delete_all.contains("DETACH DELETE"));
assert!(drop_index.contains("DROP INDEX code_embedding_index IF EXISTS"));
}
#[test]
fn test_build_count_cypher() {
let node_label = "CodeChunk";
let query = format!("MATCH (n:{}) RETURN count(n) AS total", node_label);
assert!(query.contains("count(n) AS total"));
assert!(query.contains("CodeChunk"));
}
#[test]
fn test_build_distinct_cypher() {
let node_label = "CodeChunk";
let property = "file_path";
let filter_prop = "root_path";
let cypher = format!(
"MATCH (n:{node_label}) WHERE n.{filter_prop} = $filter_val \
RETURN DISTINCT n.{property} AS val"
);
assert!(cypher.contains("DISTINCT"));
assert!(cypher.contains("n.file_path AS val"));
assert!(cypher.contains("n.root_path = $filter_val"));
}
#[test]
fn test_build_statistics_cypher() {
let node_label = "CodeChunk";
let count_query = format!("MATCH (n:{}) RETURN count(n) AS total", node_label);
let lang_query = format!(
"MATCH (n:{}) RETURN n.language AS lang, count(n) AS cnt ORDER BY cnt DESC",
node_label
);
assert!(count_query.contains("count(n) AS total"));
assert!(lang_query.contains("n.language AS lang"));
assert!(lang_query.contains("ORDER BY cnt DESC"));
}
#[test]
fn test_build_relationship_cypher() {
let label = "CodeChunk";
let rel_type = "CALLS";
let query = format!(
"MATCH (a:{label} {{file_path: $from_file, start_line: $from_line}}) \
MATCH (b:{label} {{file_path: $to_file, start_line: $to_line}}) \
MERGE (a)-[r:{rel_type}]->(b) SET r += $props",
);
assert!(query.contains("MERGE (a)-[r:CALLS]->(b)"));
assert!(query.contains("SET r += $props"));
}
#[test]
fn test_build_find_related_cypher() {
let label = "CodeChunk";
let depth = 3;
let rel_pattern = "";
let query = format!(
"MATCH (start:{label} {{file_path: $file_path, start_line: $start_line}}) \
MATCH (start)-[{rel}*1..{depth}]->(related:{label}) \
RETURN DISTINCT related",
rel = rel_pattern,
);
assert!(query.contains("*1..3"));
assert!(query.contains("RETURN DISTINCT related"));
}
#[test]
fn test_build_find_related_with_type_filter() {
let types = vec!["CALLS".to_string(), "IMPORTS".to_string()];
let rel_pattern = format!(":{}", types.join("|"));
assert_eq!(rel_pattern, ":CALLS|IMPORTS");
let label = "CodeChunk";
let depth = 2;
let query = format!(
"MATCH (start:{label} {{file_path: $file_path, start_line: $start_line}}) \
MATCH (start)-[{rel}*1..{depth}]->(related:{label}) \
RETURN DISTINCT related",
rel = rel_pattern,
);
assert!(query.contains(":CALLS|IMPORTS"));
assert!(query.contains("*1..2"));
}
#[test]
fn test_build_memory_tier_store_cypher() {
let label = "CodeChunk";
let tier = CognitiveMemoryTier::Episodic;
let query = format!(
"MERGE (n:{label} {{file_path: $file_path, start_line: $start_line}}) \
SET n += $props, n.embedding = $embedding \
SET n:{tier_label}",
tier_label = tier.as_label(),
);
assert!(query.contains("SET n:Episodic"));
assert!(query.contains("MERGE"));
}
#[test]
fn test_build_memory_tier_search_cypher() {
let index_name = "code_embedding_index";
let tier = CognitiveMemoryTier::Semantic;
let query = format!(
"CALL db.index.vector.queryNodes('{}', $limit, $vector) \
YIELD node, score \
WHERE node:{tier_label} \
RETURN node, score",
index_name,
tier_label = tier.as_label(),
);
assert!(query.contains("WHERE node:Semantic"));
assert!(query.contains("code_embedding_index"));
}
#[test]
fn test_parse_neo4j_tx_response_single() {
let payload = json!({
"results": [{
"columns": ["total"],
"data": [{"row": {"total": 42}}]
}],
"errors": []
});
let rows = payload
.get("results")
.and_then(|r| r.get(0))
.and_then(|r| r.get("data"))
.and_then(Value::as_array)
.map(|data| {
data.iter()
.filter_map(|entry| entry.get("row").cloned())
.collect::<Vec<Value>>()
})
.unwrap_or_default();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("total").unwrap().as_u64(), Some(42));
}
#[test]
fn test_parse_neo4j_tx_response_multi() {
let payload = json!({
"results": [{
"data": [
{"row": {"lang": "Rust", "cnt": 10}},
{"row": {"lang": "Python", "cnt": 5}},
{"row": {"lang": "Go", "cnt": 3}},
]
}],
"errors": []
});
let rows = payload
.get("results")
.and_then(|r| r.get(0))
.and_then(|r| r.get("data"))
.and_then(Value::as_array)
.map(|data| {
data.iter()
.filter_map(|entry| entry.get("row").cloned())
.collect::<Vec<Value>>()
})
.unwrap_or_default();
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].get("lang").unwrap().as_str(), Some("Rust"));
assert_eq!(rows[1].get("cnt").unwrap().as_u64(), Some(5));
}
#[test]
fn test_parse_neo4j_tx_response_empty() {
let payload = json!({
"results": [{"data": []}],
"errors": []
});
let rows = payload
.get("results")
.and_then(|r| r.get(0))
.and_then(|r| r.get("data"))
.and_then(Value::as_array)
.map(|data| {
data.iter()
.filter_map(|entry| entry.get("row").cloned())
.collect::<Vec<Value>>()
})
.unwrap_or_default();
assert!(rows.is_empty());
}
#[test]
fn test_parse_neo4j_tx_response_error() {
let payload = json!({
"results": [],
"errors": [{"code": "Neo.ClientError.Statement.SyntaxError", "message": "bad query"}]
});
let errors = payload
.get("errors")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(!errors.is_empty());
assert!(
errors[0]
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("bad query")
);
}
#[test]
fn test_parse_neo4j_tx_response_malformed() {
let payload = json!({"unexpected": "structure"});
let rows = payload
.get("results")
.and_then(|r| r.get(0))
.and_then(|r| r.get("data"))
.and_then(Value::as_array)
.map(|data| {
data.iter()
.filter_map(|entry| entry.get("row").cloned())
.collect::<Vec<Value>>()
})
.unwrap_or_default();
assert!(rows.is_empty());
}
#[test]
fn test_map_to_search_result_full() {
let v = json!({
"file_path": "src/main.rs",
"root_path": "/project",
"content": "fn main() {}",
"score": 0.95,
"vector_score": 0.92,
"keyword_score": 0.85,
"start_line": 1,
"end_line": 10,
"language": "Rust",
"project": "test",
"indexed_at": 1700000000,
});
let result = map_to_search_result(&v).unwrap();
assert_eq!(result.file_path, "src/main.rs");
assert_eq!(result.root_path.as_deref(), Some("/project"));
assert_eq!(result.content, "fn main() {}");
assert!((result.score - 0.95).abs() < 0.001);
assert!((result.vector_score - 0.92).abs() < 0.001);
assert!((result.keyword_score.unwrap() - 0.85).abs() < 0.001);
assert_eq!(result.start_line, 1);
assert_eq!(result.end_line, 10);
assert_eq!(result.language, "Rust");
assert_eq!(result.project.as_deref(), Some("test"));
assert_eq!(result.indexed_at, 1700000000);
}
#[test]
fn test_map_to_search_result_hybrid_scores() {
let v = json!({
"file_path": "lib.rs",
"content": "code",
"score": 0.88,
"vector_score": 0.9,
"keyword_score": 0.7,
"start_line": 5,
"end_line": 15,
"language": "Rust",
});
let result = map_to_search_result(&v).unwrap();
assert!((result.score - 0.88).abs() < 0.001);
assert!((result.vector_score - 0.9).abs() < 0.001);
assert!(result.keyword_score.is_some());
assert!((result.keyword_score.unwrap() - 0.7).abs() < 0.001);
}
#[test]
fn test_map_to_search_result_vector_only() {
let v = json!({
"file_path": "lib.rs",
"content": "code",
"score": 0.9,
"vector_score": 0.9,
"keyword_score": null,
"start_line": 1,
"end_line": 5,
});
let result = map_to_search_result(&v).unwrap();
assert!(result.keyword_score.is_none());
assert_eq!(result.language, "Unknown");
}
#[test]
fn test_map_to_search_result_missing_optional() {
let v = json!({
"file_path": "test.py",
"content": "print('hello')",
"score": 0.75,
"start_line": 1,
"end_line": 1,
});
let result = map_to_search_result(&v).unwrap();
assert!(result.root_path.is_none());
assert!(result.project.is_none());
assert!(result.keyword_score.is_none());
assert_eq!(result.vector_score, 0.0);
assert_eq!(result.indexed_at, 0);
}
#[test]
fn test_map_to_search_result_score_clamping() {
let v = json!({
"file_path": "a.rs",
"content": "x",
"score": 0.0,
"start_line": 0,
"end_line": 0,
});
let result = map_to_search_result(&v).unwrap();
assert!(result.score >= 0.0);
let v_high = json!({
"file_path": "b.rs",
"content": "y",
"score": 1.0,
"vector_score": 1.0,
"start_line": 0,
"end_line": 0,
});
let result_high = map_to_search_result(&v_high).unwrap();
assert!((result_high.score - 1.0).abs() < 0.001);
}
#[test]
fn test_map_to_search_result_returns_none_on_missing_required() {
let v = json!({"content": "x", "score": 0.5});
assert!(map_to_search_result(&v).is_none());
let v2 = json!({"file_path": "a.rs", "score": 0.5});
assert!(map_to_search_result(&v2).is_none());
let v3 = json!({"file_path": "a.rs", "content": "x"});
assert!(map_to_search_result(&v3).is_none());
}
#[tokio::test]
async fn test_build_batch_payload_single() {
let transport = MockTransport::empty();
let db = mock_db(transport);
let embeddings = vec![vec![0.1, 0.2, 0.3]];
let metadata = vec![sample_metadata("src/main.rs", 1, 10)];
let contents = vec!["fn main() {}".to_string()];
let count = db
.store_embeddings(embeddings, metadata, contents, "/project")
.await
.unwrap();
assert_eq!(count, 1);
let mock_ref = db.transport.as_ref();
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_build_batch_payload_multi() {
let transport = MockTransport::empty();
let db = mock_db(transport);
let embeddings = vec![vec![0.1, 0.2], vec![0.3, 0.4], vec![0.5, 0.6]];
let metadata = vec![
sample_metadata("a.rs", 1, 5),
sample_metadata("b.rs", 1, 5),
sample_metadata("c.rs", 1, 5),
];
let contents = vec![
"code a".to_string(),
"code b".to_string(),
"code c".to_string(),
];
let count = db
.store_embeddings(embeddings, metadata, contents, "/project")
.await
.unwrap();
assert_eq!(count, 3);
}
#[tokio::test]
async fn test_build_batch_payload_empty() {
let transport = MockTransport::empty();
let db = mock_db(transport);
let count = db
.store_embeddings(vec![], vec![], vec![], "/project")
.await
.unwrap();
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_build_batch_payload_all_metadata_fields() {
let transport = MockTransport::empty();
let db = mock_db(transport);
let meta = ChunkMetadata {
file_path: "src/lib.rs".to_string(),
root_path: Some("/custom/root".to_string()),
project: Some("my-project".to_string()),
start_line: 42,
end_line: 99,
language: Some("Rust".to_string()),
extension: Some("rs".to_string()),
file_hash: "deadbeef".to_string(),
indexed_at: 1700000000,
};
let count = db
.store_embeddings(
vec![vec![1.0, 2.0, 3.0]],
vec![meta],
vec!["pub fn foo() {}".to_string()],
"/default/root",
)
.await
.unwrap();
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_build_batch_payload_with_root_path() {
let meta = ChunkMetadata {
file_path: "file.rs".to_string(),
root_path: None,
project: None,
start_line: 0,
end_line: 0,
language: None,
extension: None,
file_hash: "hash".to_string(),
indexed_at: 0,
};
let emb = vec![1.0f32];
let node = json!({
"file_path": meta.file_path,
"root_path": meta.root_path.clone().unwrap_or_else(|| "/fallback".to_string()),
"project": meta.project,
"start_line": meta.start_line,
"end_line": meta.end_line,
"language": meta.language.clone().unwrap_or_default(),
"extension": meta.extension.clone().unwrap_or_default(),
"file_hash": meta.file_hash,
"indexed_at": meta.indexed_at,
"content": "content",
"embedding": emb,
});
assert_eq!(
node.get("root_path").unwrap().as_str().unwrap(),
"/fallback"
);
}
#[test]
fn test_build_filters_none() {
let filters = build_filters(None, None, &[], &[]);
assert!(filters.as_object().unwrap().is_empty());
}
#[test]
fn test_build_filters_project() {
let filters = build_filters(Some("my-proj"), None, &[], &[]);
assert_eq!(filters.get("project").unwrap().as_str(), Some("my-proj"));
assert!(filters.get("root_path").is_none());
}
#[test]
fn test_build_filters_extensions() {
let exts = vec!["rs".to_string(), "toml".to_string()];
let filters = build_filters(None, None, &exts, &[]);
let arr = filters.get("extension").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_str(), Some("rs"));
assert_eq!(arr[1].as_str(), Some("toml"));
}
#[test]
fn test_build_filters_languages() {
let langs = vec!["Rust".to_string()];
let filters = build_filters(None, None, &[], &langs);
let arr = filters.get("language").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0].as_str(), Some("Rust"));
}
#[test]
fn test_build_filters_combined() {
let exts = vec!["py".to_string()];
let langs = vec!["Python".to_string()];
let filters = build_filters(Some("proj"), Some("/root"), &exts, &langs);
let obj = filters.as_object().unwrap();
assert_eq!(obj.len(), 4);
assert_eq!(obj["project"].as_str(), Some("proj"));
assert_eq!(obj["root_path"].as_str(), Some("/root"));
assert_eq!(obj["extension"].as_array().unwrap().len(), 1);
assert_eq!(obj["language"].as_array().unwrap().len(), 1);
}
#[test]
fn test_build_cypher_where_none() {
let filters = build_filters(None, None, &[], &[]);
assert!(filters.as_object().unwrap().is_empty());
}
#[test]
fn test_build_cypher_where_project() {
let filters = build_filters(Some("p"), None, &[], &[]);
assert!(filters.as_object().unwrap().contains_key("project"));
assert!(!filters.as_object().unwrap().contains_key("root_path"));
}
#[test]
fn test_build_cypher_where_combined() {
let filters = build_filters(
Some("p"),
Some("/r"),
&["rs".to_string()],
&["Rust".to_string()],
);
let obj = filters.as_object().unwrap();
assert!(obj.contains_key("project"));
assert!(obj.contains_key("root_path"));
assert!(obj.contains_key("extension"));
assert!(obj.contains_key("language"));
}
#[test]
fn test_path_pattern_post_filter() {
let patterns = vec!["src/**/*.rs".to_string()];
assert!(glob_utils::matches_any_pattern("src/main.rs", &patterns));
assert!(glob_utils::matches_any_pattern(
"/project/src/lib/utils.rs",
&patterns
));
assert!(!glob_utils::matches_any_pattern(
"/project/tests/test.py",
&patterns
));
}
#[test]
fn test_auth_header_none() {
let config = NornicConfig::default();
assert!(config.username.is_none());
assert!(config.password.is_none());
}
#[test]
fn test_auth_header_bearer() {
let config = NornicConfig {
username: Some("admin".to_string()),
password: Some("s3cret".to_string()),
..Default::default()
};
assert_eq!(config.username.as_deref(), Some("admin"));
assert_eq!(config.password.as_deref(), Some("s3cret"));
}
#[test]
fn test_extract_host_http() {
assert_eq!(extract_host("http://myhost:7474"), "myhost");
}
#[test]
fn test_extract_host_https() {
assert_eq!(extract_host("https://secure.host:7474"), "secure.host");
}
#[test]
fn test_extract_host_with_port() {
assert_eq!(extract_host("http://example.com:9999/db"), "example.com");
}
#[test]
fn test_extract_host_plain() {
assert_eq!(extract_host("localhost"), "localhost");
}
#[test]
fn test_extract_host_edge_cases() {
assert_eq!(extract_host(""), "localhost");
assert_eq!(extract_host("http://"), "localhost");
assert_eq!(extract_host("http://a"), "a");
assert_eq!(
extract_host("https://nornic.internal.corp:7474/foo/bar"),
"nornic.internal.corp"
);
}
#[cfg(feature = "nornicdb-grpc")]
#[test]
fn test_grpc_transport_name() {
assert_eq!("gRPC", "gRPC");
}
#[cfg(feature = "nornicdb-grpc")]
#[test]
fn test_grpc_point_struct_mapping() {
let v = json!({
"file_path": "src/lib.rs",
"content": "pub mod foo;",
"score": 0.88,
"vector_score": 0.88,
"keyword_score": null,
"start_line": 1,
"end_line": 1,
"language": "Rust",
"project": null,
"root_path": "/project",
"indexed_at": 1700000000,
});
let result = map_to_search_result(&v).unwrap();
assert_eq!(result.file_path, "src/lib.rs");
}
#[cfg(feature = "nornicdb-bolt")]
#[test]
fn test_bolt_transport_name() {
assert_eq!("Bolt", "Bolt");
}
#[tokio::test]
async fn test_search_delegates_to_search_filtered() {
let transport = MockTransport::with_ok(vec![vec![sample_search_result_value(
"src/main.rs",
0.9,
0.9,
)]]);
let db = mock_db(transport);
let results = db
.search(vec![0.1, 0.2, 0.3], "query", 10, 0.5, None, None, false)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].file_path, "src/main.rs");
}
#[tokio::test]
async fn test_search_filtered_with_path_patterns() {
let transport = MockTransport::with_ok(vec![vec![
sample_search_result_value("src/main.rs", 0.9, 0.9),
sample_search_result_value("tests/test.rs", 0.8, 0.8),
]]);
let db = mock_db(transport);
let results = db
.search_filtered(
vec![0.1, 0.2],
"query",
10,
0.0,
None,
None,
false,
vec![],
vec![],
vec!["src/**".to_string()],
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].file_path, "src/main.rs");
}
#[tokio::test]
async fn test_search_filtered_hybrid() {
let transport =
MockTransport::with_ok(vec![vec![sample_search_result_value("a.rs", 0.85, 0.9)]]);
let db = mock_db(transport);
let results = db
.search_filtered(
vec![0.1],
"hello",
5,
0.5,
Some("proj".to_string()),
None,
true,
vec![],
vec![],
vec![],
)
.await
.unwrap();
assert_eq!(results.len(), 1);
}
#[tokio::test]
async fn test_delete_by_file_delegates() {
let transport = MockTransport::with_ok(vec![vec![json!(3)]]);
let db = mock_db(transport);
let count = db.delete_by_file("src/old.rs").await.unwrap();
assert_eq!(count, 3);
}
#[tokio::test]
async fn test_clear_executes_cypher() {
let transport = MockTransport::with_ok(vec![vec![], vec![]]);
let db = mock_db(transport);
db.clear().await.unwrap();
}
#[tokio::test]
async fn test_get_statistics_empty() {
let transport = MockTransport::with_ok(vec![vec![json!({"total": 0})], vec![]]);
let db = mock_db(transport);
let stats = db.get_statistics().await.unwrap();
assert_eq!(stats.total_points, 0);
assert_eq!(stats.total_vectors, 0);
assert!(stats.language_breakdown.is_empty());
}
#[tokio::test]
async fn test_get_statistics_with_data() {
let transport = MockTransport::with_ok(vec![
vec![json!({"total": 150})],
vec![
json!({"lang": "Rust", "cnt": 100}),
json!({"lang": "Python", "cnt": 50}),
],
]);
let db = mock_db(transport);
let stats = db.get_statistics().await.unwrap();
assert_eq!(stats.total_points, 150);
assert_eq!(stats.total_vectors, 150);
assert_eq!(stats.language_breakdown.len(), 2);
assert_eq!(stats.language_breakdown[0], ("Rust".to_string(), 100));
assert_eq!(stats.language_breakdown[1], ("Python".to_string(), 50));
}
#[tokio::test]
async fn test_flush_succeeds() {
let transport = MockTransport::empty();
let db = mock_db(transport);
db.flush().await.unwrap();
}
#[tokio::test]
async fn test_count_by_root_path_delegates() {
let transport = MockTransport::with_ok(vec![vec![json!(42)]]);
let db = mock_db(transport);
let count = db.count_by_root_path("/project").await.unwrap();
assert_eq!(count, 42);
}
#[tokio::test]
async fn test_get_indexed_files_delegates() {
let transport = MockTransport::with_ok(vec![vec![json!("src/a.rs"), json!("src/b.rs")]]);
let db = mock_db(transport);
let files = db.get_indexed_files("/project").await.unwrap();
assert_eq!(files.len(), 2);
assert!(files.contains(&"src/a.rs".to_string()));
assert!(files.contains(&"src/b.rs".to_string()));
}
#[tokio::test]
async fn test_cypher_query_returns_array() {
let transport =
MockTransport::with_ok(vec![vec![json!({"name": "Alice"}), json!({"name": "Bob"})]]);
let db = mock_db(transport);
let result = db
.cypher_query("MATCH (n) RETURN n.name AS name", json!({}))
.await
.unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
}
#[tokio::test]
async fn test_create_relationship_calls_cypher() {
let transport = MockTransport::with_ok(vec![vec![]]);
let db = mock_db(transport);
db.create_relationship(
"src/a.rs",
1,
"src/b.rs",
10,
"CALLS",
json!({"weight": 1.0}),
)
.await
.unwrap();
}
#[tokio::test]
async fn test_find_related_no_type_filter() {
let transport = MockTransport::with_ok(vec![vec![json!({"related": {
"file_path": "src/dep.rs",
"content": "use crate::foo;",
"score": 1.0,
"start_line": 1,
"end_line": 1,
}})]]);
let db = mock_db(transport);
let results = db.find_related("src/main.rs", 1, 3, None).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].file_path, "src/dep.rs");
}
#[tokio::test]
async fn test_find_related_with_type_filter() {
let transport = MockTransport::with_ok(vec![vec![]]);
let db = mock_db(transport);
let results = db
.find_related(
"src/main.rs",
1,
2,
Some(vec!["CALLS".to_string(), "IMPORTS".to_string()]),
)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
async fn test_store_with_memory_tier_episodic() {
let transport = MockTransport::with_ok(vec![vec![]]);
let db = mock_db(transport);
db.store_with_memory_tier(
vec![0.1, 0.2, 0.3],
sample_metadata("src/chat.rs", 1, 5),
"chat message".to_string(),
CognitiveMemoryTier::Episodic,
)
.await
.unwrap();
}
#[tokio::test]
async fn test_store_with_memory_tier_semantic() {
let transport = MockTransport::with_ok(vec![vec![]]);
let db = mock_db(transport);
db.store_with_memory_tier(
vec![0.4, 0.5, 0.6],
sample_metadata("src/facts.rs", 10, 20),
"known fact".to_string(),
CognitiveMemoryTier::Semantic,
)
.await
.unwrap();
}
#[tokio::test]
async fn test_search_by_memory_tier() {
let transport = MockTransport::with_ok(vec![vec![json!({
"node": {
"file_path": "src/pattern.rs",
"content": "procedural pattern",
"score": 0.95,
"start_line": 1,
"end_line": 10,
},
"score": 0.95,
})]]);
let db = mock_db(transport);
let results = db
.search_by_memory_tier(vec![0.1, 0.2], CognitiveMemoryTier::Procedural, 10)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert!((results[0].score - 0.95).abs() < 0.001);
}
#[tokio::test]
async fn test_embedding_stats() {
let transport = MockTransport::with_ok(vec![vec![
json!({"embedded_count": 500, "avg_dimension": 384.0}),
]]);
let db = mock_db(transport);
let stats = db.embedding_stats().await.unwrap();
assert_eq!(stats.get("embedded_count").unwrap().as_u64(), Some(500));
}
#[tokio::test]
async fn test_health_check_via_mock() {
let transport = MockTransport::empty();
let db = mock_db(transport);
assert!(db.health_check().await.unwrap());
}
#[tokio::test]
async fn test_authenticate_best_effort() {
let transport = MockTransport::with_ok(vec![vec![]]);
let db = mock_db(transport);
db.authenticate("user", "pass").await.unwrap();
}
#[tokio::test]
async fn test_connection_refused() {
let result = NornicDatabase::with_url("http://127.0.0.1:19999").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_empty_embeddings_store() {
let transport = MockTransport::empty();
let db = mock_db(transport);
let count = db
.store_embeddings(vec![], vec![], vec![], "/root")
.await
.unwrap();
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_initialize_index_already_exists() {
let transport = MockTransport::new(vec![
Err(anyhow::anyhow!("Index already exists")),
Ok(vec![]),
]);
let db = mock_db(transport);
db.initialize(384).await.unwrap();
}
#[tokio::test]
async fn test_clear_drop_index_error_ignored() {
let transport = MockTransport::new(vec![Ok(vec![]), Err(anyhow::anyhow!("No such index"))]);
let db = mock_db(transport);
db.clear().await.unwrap();
}
#[tokio::test]
async fn test_search_empty_results() {
let transport = MockTransport::with_ok(vec![vec![]]);
let db = mock_db(transport);
let results = db
.search(vec![0.1], "q", 10, 0.0, None, None, false)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
async fn test_search_filters_invalid_results() {
let transport = MockTransport::with_ok(vec![vec![
json!({"file_path": "a.rs", "content": "x", "score": 0.9}),
json!({"bad": "result"}),
json!({"file_path": "b.rs", "content": "y", "score": 0.8}),
]]);
let db = mock_db(transport);
let results = db
.search(vec![0.1], "q", 10, 0.0, None, None, false)
.await
.unwrap();
assert_eq!(results.len(), 2);
}
async fn skip_if_no_server() -> bool {
match NornicDatabase::with_url("http://localhost:7474").await {
Ok(db) => !db.health_check().await.unwrap_or(false),
Err(_) => true,
}
}
async fn setup_test_db() -> NornicDatabase {
let db = NornicDatabase::with_url("http://localhost:7474")
.await
.expect("Failed to connect to NornicDB");
db.clear().await.ok();
db.initialize(3).await.expect("Failed to initialize");
db
}
fn test_embedding(seed: f32) -> Vec<f32> {
vec![seed, seed + 0.1, seed + 0.2]
}
#[tokio::test]
#[ignore]
async fn test_rest_initialize_creates_index() {
if skip_if_no_server().await {
return;
}
let db = NornicDatabase::with_url("http://localhost:7474")
.await
.unwrap();
db.clear().await.ok();
let result = db.initialize(384).await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore]
async fn test_rest_clear_removes_all() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1)],
vec![sample_metadata("a.rs", 1, 5)],
vec!["code".to_string()],
"/project",
)
.await
.unwrap();
db.clear().await.unwrap();
let stats = db.get_statistics().await.unwrap_or_default();
assert_eq!(stats.total_points, 0);
}
#[tokio::test]
#[ignore]
async fn test_rest_store_single() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let count = db
.store_embeddings(
vec![test_embedding(0.1)],
vec![sample_metadata("src/main.rs", 1, 10)],
vec!["fn main() {}".to_string()],
"/project",
)
.await
.unwrap();
assert_eq!(count, 1);
}
#[tokio::test]
#[ignore]
async fn test_rest_store_multiple() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let count = db
.store_embeddings(
vec![
test_embedding(0.1),
test_embedding(0.4),
test_embedding(0.7),
],
vec![
sample_metadata("a.rs", 1, 5),
sample_metadata("b.rs", 1, 5),
sample_metadata("c.rs", 1, 5),
],
vec![
"code a".to_string(),
"code b".to_string(),
"code c".to_string(),
],
"/project",
)
.await
.unwrap();
assert_eq!(count, 3);
}
#[tokio::test]
#[ignore]
async fn test_rest_store_idempotent_upsert() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let meta = sample_metadata("src/upsert.rs", 1, 5);
let emb = test_embedding(0.5);
db.store_embeddings(
vec![emb.clone()],
vec![meta.clone()],
vec!["v1".to_string()],
"/project",
)
.await
.unwrap();
db.store_embeddings(vec![emb], vec![meta], vec!["v2".to_string()], "/project")
.await
.unwrap();
let stats = db.get_statistics().await.unwrap();
assert!(stats.total_points >= 1);
}
#[tokio::test]
#[ignore]
async fn test_rest_store_large_batch() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let n = 100;
let embeddings: Vec<Vec<f32>> = (0..n).map(|i| test_embedding(i as f32 * 0.01)).collect();
let metadata: Vec<ChunkMetadata> = (0..n)
.map(|i| sample_metadata(&format!("file_{}.rs", i), i, i + 5))
.collect();
let contents: Vec<String> = (0..n).map(|i| format!("code {}", i)).collect();
let count = db
.store_embeddings(embeddings, metadata, contents, "/project")
.await
.unwrap();
assert_eq!(count, n);
}
#[tokio::test]
#[ignore]
async fn test_rest_store_metadata_roundtrip() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let meta = ChunkMetadata {
file_path: "src/roundtrip.rs".to_string(),
root_path: Some("/my/project".to_string()),
project: Some("roundtrip-test".to_string()),
start_line: 42,
end_line: 99,
language: Some("Rust".to_string()),
extension: Some("rs".to_string()),
file_hash: "abc123def456".to_string(),
indexed_at: 1700000000,
};
db.store_embeddings(
vec![test_embedding(0.5)],
vec![meta],
vec!["pub fn roundtrip()".to_string()],
"/my/project",
)
.await
.unwrap();
let files = db.get_indexed_files("/my/project").await.unwrap();
assert!(files.contains(&"src/roundtrip.rs".to_string()));
}
#[tokio::test]
#[ignore]
async fn test_rest_search_vector() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/search.rs", 1, 5)],
vec!["searchable code".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search(test_embedding(0.5), "", 10, 0.0, None, None, false)
.await
.unwrap();
assert!(!results.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_rest_search_hybrid() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/hybrid.rs", 1, 5)],
vec!["hybrid searchable code".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search(
test_embedding(0.5),
"hybrid searchable",
10,
0.0,
None,
None,
true,
)
.await
.unwrap();
let _ = results;
}
#[tokio::test]
#[ignore]
async fn test_rest_search_min_score() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1)],
vec![sample_metadata("src/low.rs", 1, 5)],
vec!["low score".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search(test_embedding(0.9), "", 10, 0.99, None, None, false)
.await
.unwrap();
let _ = results;
}
#[tokio::test]
#[ignore]
async fn test_rest_search_limit() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
for i in 0..10 {
db.store_embeddings(
vec![test_embedding(i as f32 * 0.1)],
vec![sample_metadata(&format!("f{}.rs", i), i, i + 1)],
vec![format!("code {}", i)],
"/project",
)
.await
.unwrap();
}
let results = db
.search(test_embedding(0.5), "", 3, 0.0, None, None, false)
.await
.unwrap();
assert!(results.len() <= 3);
}
#[tokio::test]
#[ignore]
async fn test_rest_search_project_filter() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/proj.rs", 1, 5)],
vec!["project code".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search(
test_embedding(0.5),
"",
10,
0.0,
Some("nonexistent-project".to_string()),
None,
false,
)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_rest_search_empty_db() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let results = db
.search(test_embedding(0.5), "", 10, 0.0, None, None, false)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_rest_search_filtered_extension() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/ext.rs", 1, 5)],
vec!["extension filter".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search_filtered(
test_embedding(0.5),
"",
10,
0.0,
None,
None,
false,
vec!["py".to_string()],
vec![],
vec![],
)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_rest_search_filtered_language() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/lang.rs", 1, 5)],
vec!["language filter".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search_filtered(
test_embedding(0.5),
"",
10,
0.0,
None,
None,
false,
vec![],
vec!["Python".to_string()],
vec![],
)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_rest_search_filtered_combined() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/combined.rs", 1, 5)],
vec!["combined filter".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search_filtered(
test_embedding(0.5),
"",
10,
0.0,
Some("test-project".to_string()),
Some("/project".to_string()),
false,
vec!["rs".to_string()],
vec!["Rust".to_string()],
vec!["src/**".to_string()],
)
.await
.unwrap();
assert!(!results.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_rest_delete_existing() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("src/delete_me.rs", 1, 5)],
vec!["delete me".to_string()],
"/project",
)
.await
.unwrap();
let count = db.delete_by_file("src/delete_me.rs").await.unwrap();
assert!(count <= 1);
}
#[tokio::test]
#[ignore]
async fn test_rest_delete_nonexistent() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let count = db.delete_by_file("nonexistent/file.rs").await.unwrap();
assert_eq!(count, 0);
}
#[tokio::test]
#[ignore]
async fn test_rest_statistics_empty() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let stats = db.get_statistics().await.unwrap();
assert_eq!(stats.total_points, 0);
}
#[tokio::test]
#[ignore]
async fn test_rest_statistics_with_data() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1), test_embedding(0.5)],
vec![sample_metadata("a.rs", 1, 5), sample_metadata("b.rs", 1, 5)],
vec!["code a".to_string(), "code b".to_string()],
"/project",
)
.await
.unwrap();
let stats = db.get_statistics().await.unwrap();
assert!(stats.total_points >= 2);
}
#[tokio::test]
#[ignore]
async fn test_rest_count_by_root_path() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1)],
vec![sample_metadata("a.rs", 1, 5)],
vec!["code".to_string()],
"/specific/root",
)
.await
.unwrap();
let count = db.count_by_root_path("/specific/root").await.unwrap();
assert!(count >= 1);
}
#[tokio::test]
#[ignore]
async fn test_rest_get_indexed_files() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1), test_embedding(0.5)],
vec![
ChunkMetadata {
root_path: Some("/idx/root".to_string()),
..sample_metadata("src/one.rs", 1, 5)
},
ChunkMetadata {
root_path: Some("/idx/root".to_string()),
..sample_metadata("src/two.rs", 1, 5)
},
],
vec!["one".to_string(), "two".to_string()],
"/idx/root",
)
.await
.unwrap();
let files = db.get_indexed_files("/idx/root").await.unwrap();
assert!(files.len() >= 2);
}
#[cfg(feature = "nornicdb-bolt")]
#[tokio::test]
#[ignore]
async fn test_bolt_store_and_search() {
let db = match NornicDatabase::with_bolt("http://localhost:7474", "neo4j", "password").await
{
Ok(db) => db,
Err(_) => return, };
db.clear().await.ok();
db.initialize(3).await.unwrap();
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("bolt.rs", 1, 5)],
vec!["bolt code".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search(test_embedding(0.5), "", 10, 0.0, None, None, false)
.await
.unwrap();
assert!(!results.is_empty());
}
#[cfg(feature = "nornicdb-bolt")]
#[tokio::test]
#[ignore]
async fn test_bolt_delete_by_file() {
let db = match NornicDatabase::with_bolt("http://localhost:7474", "neo4j", "password").await
{
Ok(db) => db,
Err(_) => return,
};
db.clear().await.ok();
db.initialize(3).await.unwrap();
db.store_embeddings(
vec![test_embedding(0.1)],
vec![sample_metadata("bolt_del.rs", 1, 5)],
vec!["delete me".to_string()],
"/project",
)
.await
.unwrap();
let count = db.delete_by_file("bolt_del.rs").await.unwrap();
assert!(count <= 1);
}
#[cfg(feature = "nornicdb-bolt")]
#[tokio::test]
#[ignore]
async fn test_bolt_statistics() {
let db = match NornicDatabase::with_bolt("http://localhost:7474", "neo4j", "password").await
{
Ok(db) => db,
Err(_) => return,
};
db.clear().await.ok();
db.initialize(3).await.unwrap();
let stats = db.get_statistics().await.unwrap();
assert_eq!(stats.total_points, 0);
}
#[cfg(feature = "nornicdb-grpc")]
#[tokio::test]
#[ignore]
async fn test_grpc_store_and_search() {
let db = match NornicDatabase::with_grpc("http://localhost:6334").await {
Ok(db) => db,
Err(_) => return,
};
db.clear().await.ok();
db.initialize(3).await.ok();
db.store_embeddings(
vec![test_embedding(0.5)],
vec![sample_metadata("grpc.rs", 1, 5)],
vec!["grpc code".to_string()],
"/project",
)
.await
.unwrap();
let results = db
.search(test_embedding(0.5), "", 10, 0.0, None, None, false)
.await
.unwrap();
assert!(!results.is_empty());
}
#[cfg(feature = "nornicdb-grpc")]
#[tokio::test]
#[ignore]
async fn test_grpc_cypher_returns_error() {
let db = match NornicDatabase::with_grpc("http://localhost:6334").await {
Ok(db) => db,
Err(_) => return,
};
let result = db.cypher_query("RETURN 1", json!({})).await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore]
async fn test_create_relationship() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1), test_embedding(0.5)],
vec![
sample_metadata("src/caller.rs", 1, 5),
sample_metadata("src/callee.rs", 10, 20),
],
vec!["caller".to_string(), "callee".to_string()],
"/project",
)
.await
.unwrap();
let result = db
.create_relationship(
"src/caller.rs",
1,
"src/callee.rs",
10,
"CALLS",
json!({"weight": 1.0}),
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore]
async fn test_find_related_depth_1() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![test_embedding(0.1), test_embedding(0.5)],
vec![
sample_metadata("src/a.rs", 1, 5),
sample_metadata("src/b.rs", 1, 5),
],
vec!["a".to_string(), "b".to_string()],
"/project",
)
.await
.unwrap();
db.create_relationship("src/a.rs", 1, "src/b.rs", 1, "IMPORTS", json!({}))
.await
.unwrap();
let related = db.find_related("src/a.rs", 1, 1, None).await.unwrap();
assert!(!related.is_empty());
}
#[tokio::test]
#[ignore]
async fn test_find_related_depth_2() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_embeddings(
vec![
test_embedding(0.1),
test_embedding(0.3),
test_embedding(0.5),
],
vec![
sample_metadata("src/a.rs", 1, 5),
sample_metadata("src/b.rs", 1, 5),
sample_metadata("src/c.rs", 1, 5),
],
vec!["a".to_string(), "b".to_string(), "c".to_string()],
"/project",
)
.await
.unwrap();
db.create_relationship("src/a.rs", 1, "src/b.rs", 1, "CALLS", json!({}))
.await
.unwrap();
db.create_relationship("src/b.rs", 1, "src/c.rs", 1, "CALLS", json!({}))
.await
.unwrap();
let related = db.find_related("src/a.rs", 1, 2, None).await.unwrap();
assert!(related.len() >= 2);
}
#[tokio::test]
#[ignore]
async fn test_store_episodic_tier() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let result = db
.store_with_memory_tier(
test_embedding(0.5),
sample_metadata("src/chat.rs", 1, 5),
"chat message".to_string(),
CognitiveMemoryTier::Episodic,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore]
async fn test_store_semantic_tier() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
let result = db
.store_with_memory_tier(
test_embedding(0.5),
sample_metadata("src/fact.rs", 1, 5),
"known fact".to_string(),
CognitiveMemoryTier::Semantic,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore]
async fn test_search_by_tier_isolated() {
if skip_if_no_server().await {
return;
}
let db = setup_test_db().await;
db.store_with_memory_tier(
test_embedding(0.5),
sample_metadata("src/episodic.rs", 1, 5),
"episodic content".to_string(),
CognitiveMemoryTier::Episodic,
)
.await
.unwrap();
db.store_with_memory_tier(
test_embedding(0.6),
sample_metadata("src/procedural.rs", 1, 5),
"procedural content".to_string(),
CognitiveMemoryTier::Procedural,
)
.await
.unwrap();
let results = db
.search_by_memory_tier(test_embedding(0.5), CognitiveMemoryTier::Episodic, 10)
.await
.unwrap();
for r in &results {
let _ = r;
}
}
}