mod common;
use common::{mock_embedding, test_chunk, TestStore};
use cqs::normalize_for_fts;
use cqs::parser::{ChunkType, Language};
use cqs::store::SearchFilter;
use std::collections::HashSet;
use std::path::PathBuf;
#[test]
fn test_store_init() {
let store = TestStore::new();
let stats = store.stats().unwrap();
assert_eq!(stats.total_chunks, 0);
assert_eq!(stats.total_files, 0);
assert_eq!(stats.schema_version, 17); assert_eq!(stats.model_name, cqs::embedder::DEFAULT_MODEL_REPO);
}
#[test]
fn test_upsert_and_search() {
let store = TestStore::new();
let chunk = test_chunk("add", "fn add(a: i32, b: i32) -> i32 { a + b }");
let embedding = mock_embedding(1.0);
store.upsert_chunk(&chunk, &embedding, Some(12345)).unwrap();
let results = store.search_embedding_only(&embedding, 5, 0.0).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].chunk.name, "add");
assert!(
results[0].score > 0.99,
"Identical embedding should have score ~1.0"
);
}
#[test]
fn test_search_with_threshold() {
let store = TestStore::new();
let chunk1 = test_chunk("add", "fn add(a, b) { a + b }");
let chunk2 = test_chunk("subtract", "fn subtract(a, b) { a - b }");
store
.upsert_chunk(&chunk1, &mock_embedding(1.0), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(-1.0), Some(12345))
.unwrap();
let query = mock_embedding(0.9);
let results = store.search_embedding_only(&query, 5, 0.5).unwrap();
assert!(results.iter().any(|r| r.chunk.name == "add"));
}
#[test]
fn test_search_limit() {
let store = TestStore::new();
for i in 0..10 {
let chunk = test_chunk(&format!("fn{}", i), &format!("fn fn{}() {{}}", i));
let emb = mock_embedding(1.0 + i as f32 * 0.01);
store.upsert_chunk(&chunk, &emb, Some(12345)).unwrap();
}
let query = mock_embedding(1.0);
let results = store.search_embedding_only(&query, 3, 0.0).unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_search_filtered_by_language() {
let store = TestStore::new();
let rust_chunk = test_chunk("rust_fn", "fn rust_fn() {}");
store
.upsert_chunk(&rust_chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let mut py_chunk = test_chunk("py_fn", "def py_fn(): pass");
py_chunk.language = Language::Python;
py_chunk.file = PathBuf::from("test.py");
store
.upsert_chunk(&py_chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let filter = SearchFilter {
languages: Some(vec![Language::Rust]),
path_pattern: None,
..Default::default()
};
let results = store
.search_filtered(&mock_embedding(1.0), &filter, 10, 0.0)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].chunk.name, "rust_fn");
}
#[test]
fn test_needs_reindex_not_indexed() {
let store = TestStore::new();
let dir = tempfile::TempDir::new().unwrap();
let file_path = dir.path().join("new_file.rs");
std::fs::write(&file_path, "fn test() {}").unwrap();
let needs = store.needs_reindex(&file_path).unwrap();
assert!(
needs.is_some(),
"File not in index should need reindexing (return Some(mtime))"
);
}
#[test]
fn test_delete_by_origin() {
let store = TestStore::new();
let chunk1 = test_chunk("fn1", "fn fn1() {}");
let mut chunk2 = test_chunk("fn2", "fn fn2() {}");
chunk2.file = PathBuf::from("other.rs");
chunk2.id = format!("other.rs:1:{}", &chunk2.content_hash[..8]);
store
.upsert_chunk(&chunk1, &mock_embedding(1.0), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(1.0), Some(12345))
.unwrap();
let deleted = store.delete_by_origin(&PathBuf::from("test.rs")).unwrap();
assert_eq!(deleted, 1);
let results = store
.search_embedding_only(&mock_embedding(1.0), 10, 0.0)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].chunk.name, "fn2");
let deleted_again = store.delete_by_origin(&PathBuf::from("test.rs")).unwrap();
assert_eq!(deleted_again, 0);
}
#[test]
fn test_prune_missing() {
let store = TestStore::new();
let chunk1 = test_chunk("fn1", "fn fn1() {}");
let mut chunk2 = test_chunk("fn2", "fn fn2() {}");
chunk2.file = PathBuf::from("other.rs");
chunk2.id = format!("other.rs:1:{}", &chunk2.content_hash[..8]);
store
.upsert_chunk(&chunk1, &mock_embedding(1.0), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(1.0), Some(12345))
.unwrap();
let existing: HashSet<PathBuf> = vec![PathBuf::from("test.rs")].into_iter().collect();
let pruned = store.prune_missing(&existing).unwrap();
assert_eq!(pruned, 1);
let results = store
.search_embedding_only(&mock_embedding(1.0), 10, 0.0)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].chunk.name, "fn1");
}
#[test]
fn test_get_embeddings_by_hashes_single() {
let store = TestStore::new();
let content = "fn test() { 42 }";
let chunk = test_chunk("test", content);
let embedding = mock_embedding(0.5);
store.upsert_chunk(&chunk, &embedding, Some(12345)).unwrap();
let found = store
.get_embeddings_by_hashes(&[&chunk.content_hash])
.unwrap();
assert!(
found.contains_key(&chunk.content_hash),
"Should find embedding for existing content hash"
);
let not_found = store.get_embeddings_by_hashes(&["nonexistent"]).unwrap();
assert!(
not_found.is_empty(),
"Should return empty map for non-existent hash"
);
}
#[test]
fn test_get_embeddings_by_hashes() {
let store = TestStore::new();
let chunk1 = test_chunk("fn1", "fn fn1() { 1 }");
let chunk2 = test_chunk("fn2", "fn fn2() { 2 }");
let emb1 = mock_embedding(0.1);
let emb2 = mock_embedding(0.2);
store.upsert_chunk(&chunk1, &emb1, Some(12345)).unwrap();
store.upsert_chunk(&chunk2, &emb2, Some(12345)).unwrap();
let hashes = vec![
chunk1.content_hash.as_str(),
chunk2.content_hash.as_str(),
"nonexistent_hash",
];
let result = store.get_embeddings_by_hashes(&hashes).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains_key(&chunk1.content_hash));
assert!(result.contains_key(&chunk2.content_hash));
assert!(!result.contains_key("nonexistent_hash"));
let empty_result = store.get_embeddings_by_hashes(&[]).unwrap();
assert!(empty_result.is_empty());
}
#[test]
fn test_stats() {
let store = TestStore::new();
let chunk1 = test_chunk("fn1", "fn fn1() {}");
let mut chunk2 = test_chunk("fn2", "fn fn2() {}");
chunk2.file = PathBuf::from("other.rs");
chunk2.id = format!("other.rs:1:{}", &chunk2.content_hash[..8]);
let mut chunk3 = test_chunk("method1", "fn method1(&self) {}");
chunk3.chunk_type = ChunkType::Method;
store
.upsert_chunk(&chunk1, &mock_embedding(1.0), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(1.0), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk3, &mock_embedding(1.0), Some(12345))
.unwrap();
let stats = store.stats().unwrap();
assert_eq!(stats.total_chunks, 3);
assert_eq!(stats.total_files, 2);
assert_eq!(
*stats.chunks_by_language.get(&Language::Rust).unwrap_or(&0),
3
);
assert_eq!(
*stats.chunks_by_type.get(&ChunkType::Function).unwrap_or(&0),
2
);
assert_eq!(
*stats.chunks_by_type.get(&ChunkType::Method).unwrap_or(&0),
1
);
}
#[test]
fn test_fts_search() {
let store = TestStore::new();
let chunk1 = test_chunk(
"parseConfigFile",
"fn parseConfigFile() { /* parse config */ }",
);
let chunk2 = test_chunk(
"loadUserSettings",
"fn loadUserSettings() { /* load settings */ }",
);
let chunk3 = test_chunk("calculateTotal", "fn calculateTotal() { /* math */ }");
store
.upsert_chunk(&chunk1, &mock_embedding(0.1), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(0.2), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk3, &mock_embedding(0.3), Some(12345))
.unwrap();
let results = store.search_fts("config", 5).unwrap();
assert!(
!results.is_empty(),
"FTS should find 'config' in parseConfigFile"
);
assert!(results
.iter()
.any(|id| id.contains("parseConfigFile") || id.starts_with("test.rs")));
let results = store.search_fts("parse file", 5).unwrap();
assert!(
!results.is_empty(),
"FTS should find 'parse file' via normalization"
);
let results = store.search_fts("settings", 5).unwrap();
assert!(!results.is_empty(), "FTS should find 'settings'");
let results = store.search_fts("xyznonexistent", 5).unwrap();
assert!(
results.is_empty(),
"FTS should return empty for nonexistent term"
);
}
#[test]
fn test_rrf_search() {
let store = TestStore::new();
let chunk1 = test_chunk("handleError", "fn handleError(err: Error) { log(err); }");
let chunk2 = test_chunk(
"processData",
"fn processData(data: Vec<u8>) { /* process */ }",
);
store
.upsert_chunk(&chunk1, &mock_embedding(0.5), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(0.5), Some(12345))
.unwrap();
let filter = SearchFilter {
enable_rrf: true,
query_text: "error handling".to_string(),
..Default::default()
};
let results = store
.search_filtered(&mock_embedding(0.5), &filter, 5, 0.0)
.unwrap();
assert!(!results.is_empty(), "RRF search should return results");
}
#[test]
fn test_normalize_for_fts() {
assert_eq!(normalize_for_fts("parseConfigFile"), "parse config file");
assert_eq!(normalize_for_fts("parse_config_file"), "parse config file");
assert_eq!(normalize_for_fts("ParseConfigFile"), "parse config file");
assert_eq!(
normalize_for_fts("fn parseConfig() { return value; }"),
"fn parse config return value"
);
assert_eq!(
normalize_for_fts("parseVersion2Config"),
"parse version2 config"
);
assert_eq!(normalize_for_fts("hello world"), "hello world");
assert_eq!(normalize_for_fts(""), "");
assert_eq!(normalize_for_fts("parse"), "parse");
}
#[test]
fn test_normalize_for_fts_strips_fts5_special_chars() {
assert_eq!(normalize_for_fts("test*"), "test");
assert_eq!(normalize_for_fts("*test*"), "test");
assert_eq!(normalize_for_fts("\"exact phrase\""), "exact phrase");
assert_eq!(normalize_for_fts("content:test"), "content test");
assert_eq!(normalize_for_fts("name:foo"), "name foo");
assert_eq!(normalize_for_fts("-excluded"), "excluded");
assert_eq!(normalize_for_fts("+required"), "required");
assert_eq!(normalize_for_fts("(test)"), "test");
assert_eq!(normalize_for_fts("test^2"), "test 2");
assert_eq!(normalize_for_fts("test/other"), "test other");
assert_eq!(normalize_for_fts("*\"content:*\""), "content");
assert_eq!(
normalize_for_fts("test; DROP TABLE--"),
"test d r o p t a b l e"
);
}
#[test]
fn test_future_schema_version_rejected() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("future.db");
{
let store = cqs::store::Store::open(&db_path).unwrap();
store.init(&cqs::store::ModelInfo::default()).unwrap();
}
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.connect(&format!("sqlite://{}", db_path.display()))
.await
.unwrap();
sqlx::query("UPDATE metadata SET value = '999' WHERE key = 'schema_version'")
.execute(&pool)
.await
.unwrap();
pool.close().await;
});
let result = cqs::store::Store::open(&db_path);
match result {
Ok(_) => panic!("Future schema version should be rejected"),
Err(e) => {
let err_msg = e.to_string();
assert!(
err_msg.contains("newer") || err_msg.contains("upgrade"),
"Error should mention newer version: {}",
err_msg
);
}
}
}
#[test]
fn test_old_schema_version_rejected() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("old.db");
{
let store = cqs::store::Store::open(&db_path).unwrap();
store.init(&cqs::store::ModelInfo::default()).unwrap();
}
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.connect(&format!("sqlite://{}", db_path.display()))
.await
.unwrap();
sqlx::query("UPDATE metadata SET value = '5' WHERE key = 'schema_version'")
.execute(&pool)
.await
.unwrap();
pool.close().await;
});
let result = cqs::store::Store::open(&db_path);
match result {
Ok(_) => panic!("Old schema version should be rejected"),
Err(e) => {
let err_msg = e.to_string();
assert!(
err_msg.contains("mismatch") || err_msg.contains("--force"),
"Error should mention mismatch or rebuild: {}",
err_msg
);
}
}
}
#[test]
fn test_model_mismatch_rejected() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("model.db");
{
let store = cqs::store::Store::open(&db_path).unwrap();
store.init(&cqs::store::ModelInfo::default()).unwrap();
}
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.connect(&format!("sqlite://{}", db_path.display()))
.await
.unwrap();
sqlx::query("UPDATE metadata SET value = 'different-model' WHERE key = 'model_name'")
.execute(&pool)
.await
.unwrap();
pool.close().await;
});
let result = cqs::store::Store::open(&db_path);
assert!(
result.is_ok(),
"Store::open should accept any model name (configurable models): {:?}",
result.err()
);
}
#[test]
fn test_embedding_batches() {
let store = TestStore::new();
for i in 0..25 {
let chunk = test_chunk(&format!("fn{}", i), &format!("fn fn{}() {{}}", i));
let emb = mock_embedding(i as f32);
store.upsert_chunk(&chunk, &emb, Some(12345)).unwrap();
}
let batches: Vec<_> = store.embedding_batches(10).collect();
assert_eq!(batches.len(), 3, "Expected 3 batches for 25 chunks");
let batch1 = batches[0].as_ref().unwrap();
let batch2 = batches[1].as_ref().unwrap();
let batch3 = batches[2].as_ref().unwrap();
assert_eq!(batch1.len(), 10, "First batch should have 10 embeddings");
assert_eq!(batch2.len(), 10, "Second batch should have 10 embeddings");
assert_eq!(batch3.len(), 5, "Third batch should have 5 embeddings");
let total: usize = batches
.iter()
.filter_map(|b| b.as_ref().ok())
.map(|b| b.len())
.sum();
assert_eq!(total, 25);
}
#[test]
fn test_embedding_batches_empty() {
let store = TestStore::new();
let batches: Vec<_> = store.embedding_batches(10).collect();
assert!(batches.is_empty(), "Empty store should yield no batches");
}
#[test]
fn test_embedding_batches_exact_multiple() {
let store = TestStore::new();
for i in 0..20 {
let chunk = test_chunk(&format!("fn{}", i), &format!("fn fn{}() {{}}", i));
let emb = mock_embedding(i as f32);
store.upsert_chunk(&chunk, &emb, Some(12345)).unwrap();
}
let batches: Vec<_> = store.embedding_batches(10).collect();
assert_eq!(batches.len(), 2, "20 chunks / 10 batch = 2 batches");
for batch in &batches {
let b = batch.as_ref().unwrap();
assert_eq!(b.len(), 10);
}
}
#[test]
fn test_fts_unicode_function_names() {
let store = TestStore::new();
let mut chunk = test_chunk("计算", "fn 计算() { /* calculate */ }");
chunk.content = "fn 计算() { /* calculate */ }".to_string();
chunk.name = "计算".to_string();
store
.upsert_chunk(&chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let filter = SearchFilter::default().with_query("计算");
let results = store.search_filtered(&mock_embedding(1.0), &filter, 5, 0.0);
assert!(results.is_ok(), "FTS search should not fail on Unicode");
}
#[test]
fn test_fts_emoji_in_comments() {
let store = TestStore::new();
let mut chunk = test_chunk("emoji_fn", "fn emoji_fn() { /* 🚀 launch */ }");
chunk.content = "fn emoji_fn() { /* 🚀 launch */ }".to_string();
store
.upsert_chunk(&chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let filter = SearchFilter::default().with_query("launch");
let results = store.search_filtered(&mock_embedding(1.0), &filter, 5, 0.0);
assert!(
results.is_ok(),
"FTS search should not fail with emoji in content"
);
}
#[test]
fn test_normalize_for_fts_unicode() {
assert!(!normalize_for_fts("hello 世界").is_empty());
assert!(!normalize_for_fts("emoji 🎉 test").is_empty());
assert!(!normalize_for_fts("diacritics: café résumé").is_empty());
assert!(!normalize_for_fts("rtl: שלום").is_empty());
}
#[test]
fn test_get_call_graph() {
use cqs::parser::{CallSite, FunctionCalls};
let store = TestStore::new();
let chunk_a = test_chunk("func_a", "fn func_a() { func_b(); func_c(); }");
let chunk_b = test_chunk("func_b", "fn func_b() { func_c(); }");
let chunk_c = test_chunk("func_c", "fn func_c() {}");
let emb = mock_embedding(1.0);
store.upsert_chunk(&chunk_a, &emb, Some(12345)).unwrap();
store.upsert_chunk(&chunk_b, &emb, Some(12345)).unwrap();
store.upsert_chunk(&chunk_c, &emb, Some(12345)).unwrap();
let function_calls = vec![
FunctionCalls {
name: "func_a".to_string(),
line_start: 1,
calls: vec![
CallSite {
callee_name: "func_b".to_string(),
line_number: 1,
},
CallSite {
callee_name: "func_c".to_string(),
line_number: 1,
},
],
},
FunctionCalls {
name: "func_b".to_string(),
line_start: 5,
calls: vec![CallSite {
callee_name: "func_c".to_string(),
line_number: 5,
}],
},
];
store
.upsert_function_calls(&PathBuf::from("test.rs"), &function_calls)
.unwrap();
let graph = store.get_call_graph().unwrap();
assert_eq!(
graph.forward.get("func_a").map(|v| v.len()),
Some(2),
"func_a should call 2 functions"
);
assert_eq!(
graph.forward.get("func_b").map(|v| v.len()),
Some(1),
"func_b should call 1 function"
);
assert!(
!graph.forward.contains_key("func_c"),
"func_c should call nothing"
);
assert_eq!(
graph.reverse.get("func_c").map(|v| v.len()),
Some(2),
"func_c should be called by 2 functions"
);
assert_eq!(
graph.reverse.get("func_b").map(|v| v.len()),
Some(1),
"func_b should be called by 1 function"
);
assert!(
!graph.reverse.contains_key("func_a"),
"func_a should not be called by anyone"
);
}
#[test]
fn test_all_chunk_identities() {
let store = TestStore::new();
let chunk1 = test_chunk("fn1", "fn fn1() {}");
let mut chunk2 = test_chunk("fn2", "fn fn2() {}");
chunk2.file = PathBuf::from("other.rs");
chunk2.id = format!("other.rs:1:{}", &chunk2.content_hash[..8]);
chunk2.line_start = 10;
let emb = mock_embedding(1.0);
store.upsert_chunk(&chunk1, &emb, Some(12345)).unwrap();
store.upsert_chunk(&chunk2, &emb, Some(12345)).unwrap();
let identities = store.all_chunk_identities().unwrap();
assert_eq!(identities.len(), 2, "Should return 2 chunk identities");
let id1 = identities.iter().find(|i| i.name == "fn1").unwrap();
assert_eq!(id1.file.to_string_lossy(), "test.rs");
assert_eq!(id1.language, Language::Rust);
assert_eq!(id1.line_start, 1);
let id2 = identities.iter().find(|i| i.name == "fn2").unwrap();
assert_eq!(id2.file.to_string_lossy(), "other.rs");
assert_eq!(id2.line_start, 10);
}
#[test]
fn test_get_chunk_with_embedding() {
let store = TestStore::new();
let chunk = test_chunk("test_fn", "fn test_fn() { 42 }");
let embedding = mock_embedding(0.75);
store.upsert_chunk(&chunk, &embedding, Some(12345)).unwrap();
let result = store.get_chunk_with_embedding(&chunk.id).unwrap();
assert!(result.is_some(), "Should find the chunk");
let (retrieved_chunk, retrieved_emb) = result.unwrap();
assert_eq!(retrieved_chunk.name, "test_fn");
assert_eq!(retrieved_chunk.id, chunk.id);
assert_eq!(retrieved_emb.as_slice().len(), embedding.as_slice().len());
}
#[test]
fn test_get_chunk_with_embedding_nonexistent() {
let store = TestStore::new();
let result = store
.get_chunk_with_embedding("nonexistent:1:abcd1234")
.unwrap();
assert!(
result.is_none(),
"Should return None for non-existent chunk"
);
}
#[test]
fn test_store_close() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("test_close.db");
{
let store = cqs::store::Store::open(&db_path).unwrap();
store.init(&cqs::store::ModelInfo::default()).unwrap();
let chunk = test_chunk("test_fn", "fn test_fn() {}");
let emb = mock_embedding(1.0);
store.upsert_chunk(&chunk, &emb, Some(12345)).unwrap();
store.close().unwrap();
}
let store = cqs::store::Store::open(&db_path).unwrap();
let stats = store.stats().unwrap();
assert_eq!(stats.total_chunks, 1, "Chunk should persist after close");
let results = store
.search_embedding_only(&mock_embedding(1.0), 5, 0.0)
.unwrap();
assert_eq!(results.len(), 1, "Should find the persisted chunk");
}
#[test]
fn test_fts_empty_string() {
let store = TestStore::new();
let chunk = test_chunk("test_fn", "fn test_fn() {}");
store
.upsert_chunk(&chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let results = store.search_fts("", 5).unwrap();
assert!(
results.is_empty(),
"Empty query should return no results or all results"
);
}
#[test]
fn test_fts_special_characters() {
let store = TestStore::new();
let chunk1 = test_chunk("foo::bar", "fn foo::bar() {}");
let chunk2 = test_chunk("Vec<T>", "struct Vec<T> {}");
let chunk3 = test_chunk("quoted_name", "fn \"quoted\" name");
store
.upsert_chunk(&chunk1, &mock_embedding(0.1), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(0.2), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk3, &mock_embedding(0.3), Some(12345))
.unwrap();
let results = store.search_fts("foo::bar", 5);
assert!(results.is_ok(), "FTS should handle :: in query");
let results = store.search_fts("Vec<T>", 5);
assert!(results.is_ok(), "FTS should handle angle brackets in query");
let results = store.search_fts("\"quoted\"", 5);
assert!(results.is_ok(), "FTS should handle quotes in query");
}
#[test]
fn test_fts_sql_injection_characters() {
let store = TestStore::new();
let chunk = test_chunk("test_fn", "fn test_fn() {}");
store
.upsert_chunk(&chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let results = store.search_fts("'; DROP TABLE chunks--", 5);
assert!(
results.is_ok(),
"FTS should safely handle SQL injection attempts"
);
let stats = store.stats().unwrap();
assert_eq!(stats.total_chunks, 1, "Database should remain intact");
}
#[test]
fn test_fts_unicode_queries() {
let store = TestStore::new();
let mut chunk1 = test_chunk("calculate", "fn calculate() {}");
chunk1.content = "fn calculate() { /* 计算 */ }".to_string();
let mut chunk2 = test_chunk("über", "fn über() {}");
chunk2.name = "über".to_string();
store
.upsert_chunk(&chunk1, &mock_embedding(0.1), Some(12345))
.unwrap();
store
.upsert_chunk(&chunk2, &mock_embedding(0.2), Some(12345))
.unwrap();
let results = store.search_fts("计算", 5);
assert!(results.is_ok(), "FTS should handle CJK characters");
let results = store.search_fts("über", 5);
assert!(results.is_ok(), "FTS should handle diacritics");
}
#[test]
fn test_fts_very_long_query() {
let store = TestStore::new();
let chunk = test_chunk("test_fn", "fn test_fn() {}");
store
.upsert_chunk(&chunk, &mock_embedding(1.0), Some(12345))
.unwrap();
let long_query = "test ".repeat(250); let results = store.search_fts(&long_query, 5);
assert!(
results.is_ok(),
"FTS should handle very long queries without panic"
);
}
#[test]
fn test_check_origins_stale_across_batch_boundary() {
use tempfile::TempDir;
let store = TestStore::new();
let project_dir = TempDir::new().unwrap();
let root = project_dir.path();
let count = 950;
let emb = mock_embedding(1.0);
for i in 0..count {
let filename = format!("file_{:04}.rs", i);
let filepath = root.join(&filename);
std::fs::write(&filepath, format!("fn f{}() {{}}", i)).unwrap();
let content = format!("fn f{}() {{ {} }}", i, i);
let hash = blake3::hash(content.as_bytes()).to_hex().to_string();
let chunk = cqs::parser::Chunk {
id: format!("{}:1:{}", &filename, &hash[..8]),
file: PathBuf::from(&filename),
language: Language::Rust,
chunk_type: ChunkType::Function,
name: format!("f{}", i),
signature: format!("fn f{}()", i),
content,
doc: None,
line_start: 1,
line_end: 3,
content_hash: hash,
parent_id: None,
window_idx: None,
parent_type_name: None,
};
let mtime = if i < count / 2 {
1000i64
} else {
i64::MAX / 2
};
store
.upsert_chunks_batch(&[(chunk, emb.clone())], Some(mtime))
.unwrap();
}
let origins: Vec<String> = (0..count).map(|i| format!("file_{:04}.rs", i)).collect();
let origin_refs: Vec<&str> = origins.iter().map(|s| s.as_str()).collect();
let stale = store.check_origins_stale(&origin_refs, root).unwrap();
for i in 0..count / 2 {
let origin = format!("file_{:04}.rs", i);
assert!(
stale.contains(&origin),
"Origin {} should be stale (old mtime), batch boundary at 900",
origin
);
}
for i in count / 2..count {
let origin = format!("file_{:04}.rs", i);
assert!(
!stale.contains(&origin),
"Origin {} should be fresh (future mtime)",
origin
);
}
let expected_stale = count / 2;
assert_eq!(
stale.len(),
expected_stale,
"Expected {} stale origins across batch boundary, got {}",
expected_stale,
stale.len()
);
}
#[test]
fn test_open_readonly_on_initialized_store() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("readonly.db");
{
let store = cqs::store::Store::open(&db_path).unwrap();
store.init(&cqs::store::ModelInfo::default()).unwrap();
}
let ro = cqs::store::Store::open_readonly(&db_path).unwrap();
let stats = ro.stats().unwrap();
assert_eq!(stats.total_chunks, 0);
assert_eq!(stats.schema_version, 17);
assert_eq!(stats.model_name, cqs::embedder::DEFAULT_MODEL_REPO);
}
#[test]
fn test_open_readonly_on_missing_file_fails() {
let dir = tempfile::TempDir::new().unwrap();
let result = cqs::store::Store::open_readonly(&dir.path().join("nonexistent.db"));
assert!(result.is_err(), "open_readonly on missing file should fail");
}
#[test]
fn test_open_readonly_preserves_data() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("readonly_data.db");
{
let store = cqs::store::Store::open(&db_path).unwrap();
store.init(&cqs::store::ModelInfo::default()).unwrap();
let chunk = test_chunk("preserved_fn", "fn preserved_fn() { 42 }");
let emb = mock_embedding(1.0);
store.upsert_chunk(&chunk, &emb, Some(12345)).unwrap();
}
let ro = cqs::store::Store::open_readonly(&db_path).unwrap();
let stats = ro.stats().unwrap();
assert_eq!(
stats.total_chunks, 1,
"Chunk should be visible in read-only mode"
);
assert_eq!(stats.total_files, 1);
let results = ro
.search_embedding_only(&mock_embedding(1.0), 5, 0.0)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].chunk.name, "preserved_fn");
}