#![cfg(feature = "slow-tests")]
use assert_cmd::Command;
use rusqlite::Connection;
use serial_test::serial;
use tempfile::TempDir;
fn sgr_cmd() -> Command {
let mock_dir = common::mock_llm_path();
let mut c = Command::cargo_bin("sqlite-graphrag").expect("sqlite-graphrag binary not found");
c.env("PATH", common::prepend_path(&mock_dir));
c
}
#[path = "common/mod.rs"]
mod common;
fn init_isolated_db() -> (TempDir, std::path::PathBuf) {
let tmp = TempDir::new().expect("TempDir must be created");
let db_path = tmp.path().join("test.sqlite");
sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
(tmp, db_path)
}
fn conn_ro(db_path: &std::path::Path) -> Connection {
Connection::open(db_path).expect("database connection must work")
}
fn table_exists(conn: &Connection, name: &str) -> bool {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table','view') AND name = ?1",
rusqlite::params![name],
|row| row.get(0),
)
.unwrap_or(0);
count > 0
}
fn trigger_exists(conn: &Connection, name: &str) -> bool {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger' AND name = ?1",
rusqlite::params![name],
|row| row.get(0),
)
.unwrap_or(0);
count > 0
}
fn index_exists(conn: &Connection, name: &str) -> bool {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = ?1",
rusqlite::params![name],
|row| row.get(0),
)
.unwrap_or(0);
count > 0
}
#[test]
#[serial]
fn init_creates_13_migrations_v001_to_v013() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let versions: Vec<i64> = {
let mut stmt = conn
.prepare("SELECT version FROM refinery_schema_history ORDER BY version ASC")
.expect("prepare must work");
stmt.query_map([], |row| row.get(0))
.expect("query must work")
.map(|r| r.expect("row must be readable"))
.collect()
};
assert_eq!(
versions.len(),
13,
"exactly 13 migrations must be applied, found: {versions:?}"
);
assert_eq!(
versions,
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
"expected versions V001-V013"
);
}
#[test]
#[serial]
fn trigger_trg_fts_ai_exists() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
assert!(
trigger_exists(&conn, "trg_fts_ai"),
"trigger trg_fts_ai must exist after V004"
);
}
#[test]
#[serial]
fn trigger_trg_fts_ad_exists() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
assert!(
trigger_exists(&conn, "trg_fts_ad"),
"trigger trg_fts_ad must exist after V004"
);
}
#[test]
#[serial]
fn trigger_trg_fts_au_absent_handled_in_rust() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
assert!(
!trigger_exists(&conn, "trg_fts_au"),
"trigger trg_fts_au must NOT exist — FTS5 sync is handled in Rust (edit.rs, rename.rs, restore.rs)"
);
}
#[test]
#[serial]
fn memory_embeddings_blob_dim_384() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let ddl: String = conn
.query_row(
"SELECT sql FROM sqlite_master WHERE name = 'memory_embeddings'",
[],
|row| row.get(0),
)
.expect("memory_embeddings must exist in sqlite_master");
assert!(
ddl.contains("BLOB"),
"memory_embeddings must declare embedding as BLOB, DDL was: {ddl}"
);
assert!(
ddl.contains("dim"),
"memory_embeddings must declare a dim column, DDL was: {ddl}"
);
assert!(
ddl.contains("384"),
"memory_embeddings must default dim to 384, DDL was: {ddl}"
);
let vec_present: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'vec_memories'",
[],
|row| row.get(0),
)
.unwrap_or(1);
assert_eq!(
vec_present, 0,
"vec_memories must NOT exist after V013, but it is still present"
);
}
#[test]
#[serial]
fn memory_embeddings_partition_indexes() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let has_ns_index: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'idx_memory_embeddings_ns'",
[],
|row| row.get(0),
)
.unwrap_or(0);
assert_eq!(
has_ns_index, 1,
"idx_memory_embeddings_ns must exist (namespace partition)"
);
let has_source_index: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'idx_memory_embeddings_source'",
[],
|row| row.get(0),
)
.unwrap_or(0);
assert_eq!(
has_source_index, 1,
"idx_memory_embeddings_source must exist (source partition)"
);
}
#[test]
#[serial]
fn fts_memories_tokenizer_unicode61_remove_diacritics() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let ddl: String = conn
.query_row(
"SELECT sql FROM sqlite_master WHERE name = 'fts_memories'",
[],
|row| row.get(0),
)
.expect("fts_memories must exist in sqlite_master");
assert!(
ddl.contains("unicode61"),
"fts_memories must use the unicode61 tokenizer, DDL: {ddl}"
);
assert!(
ddl.contains("remove_diacritics"),
"fts_memories must declare remove_diacritics, DDL: {ddl}"
);
}
#[test]
#[serial]
fn fts5_matching_with_accents_cafe_cafe() {
let tmp = TempDir::new().expect("TempDir must be created");
let db_path = tmp.path().join("test.sqlite");
sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.env("SQLITE_GRAPHRAG_NAMESPACE", "global")
.args([
"--skip-memory-guard",
"remember",
"--name",
"nota-cafe",
"--type",
"user",
"--description",
"note about café",
"--body",
"Brazilian café is famous worldwide for its quality",
])
.assert()
.success();
let conn = conn_ro(&db_path);
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM fts_memories WHERE fts_memories MATCH 'cafe'",
[],
|row| row.get(0),
)
.expect("FTS5 query must work");
assert!(
count >= 1,
"FTS5 with remove_diacritics must match 'café' when searching 'cafe', count={count}"
);
}
#[test]
#[serial]
fn all_main_tables_exist_after_init() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let tables = [
"schema_meta",
"memories",
"memory_versions",
"memory_chunks",
"entities",
"relationships",
"memory_entities",
"memory_relationships",
"fts_memories",
];
for name in tables {
assert!(
table_exists(&conn, name),
"table '{name}' must exist after init"
);
}
}
#[test]
#[serial]
fn main_indexes_exist_after_init() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let indexes = [
"idx_memories_ns_type",
"idx_memories_ns_live",
"idx_memories_body_hash",
"idx_entities_ns",
"idx_me_entity",
"idx_relationships_source",
"idx_relationships_target",
"idx_relationships_ns",
"idx_relationships_ns_relation",
"idx_entities_namespace_degree",
"idx_memory_chunks_memory_id",
"idx_memory_relationships_relationship_id",
];
for name in indexes {
assert!(
index_exists(&conn, name),
"index '{name}' must exist after init"
);
}
}
#[test]
#[serial]
fn schema_meta_required_keys_exist() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let expected_keys = [
"schema_version",
"model",
"dim",
"created_at",
"namespace_initial",
];
for key in expected_keys {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM schema_meta WHERE key = ?1",
rusqlite::params![key],
|row| row.get(0),
)
.expect("schema_meta query must work");
assert!(count > 0, "schema_meta must contain key '{key}' after init");
}
}
#[test]
#[serial]
fn schema_version_meta_equals_13() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
let version: String = conn
.query_row(
"SELECT value FROM schema_meta WHERE key = 'schema_version'",
[],
|row| row.get(0),
)
.expect("schema_version must exist in schema_meta");
assert_eq!(
version, "13",
"schema_version in schema_meta must be '13' after V013"
);
}
#[test]
#[serial]
fn v009_document_type_lifecycle_e2e() {
let tmp = TempDir::new().expect("TempDir must be created");
let db_path = tmp.path().join("test.sqlite");
sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.env("SQLITE_GRAPHRAG_NAMESPACE", "global")
.args([
"--skip-memory-guard",
"remember",
"--name",
"doc-test",
"--type",
"document",
"--description",
"test doc",
"--body",
"Sample document body for e2e test",
"--skip-extraction",
])
.output()
.expect("remember must run");
assert!(
output.status.success(),
"remember failed: status={:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args([
"--skip-memory-guard",
"list",
"--type",
"document",
"--json",
])
.output()
.expect("list must run");
assert!(
output.status.success(),
"list failed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("list output must be valid JSON");
let items = json["items"]
.as_array()
.expect("list response must contain `items` array");
assert_eq!(items.len(), 1, "expected exactly 1 document, got {items:?}");
assert_eq!(items[0]["type"], "document");
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "recall", "Sample", "--json"])
.output()
.expect("recall must run");
assert!(
output.status.success(),
"recall failed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("recall output must be valid JSON");
let results = json["results"]
.as_array()
.expect("recall response must contain `results` array");
assert!(
!results.is_empty(),
"recall must return at least one match for 'Sample', got: {results:?}"
);
}
#[test]
#[serial]
fn v009_note_type_lifecycle_e2e() {
let tmp = TempDir::new().expect("TempDir must be created");
let db_path = tmp.path().join("test.sqlite");
sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.env("SQLITE_GRAPHRAG_NAMESPACE", "global")
.args([
"--skip-memory-guard",
"remember",
"--name",
"note-test",
"--type",
"note",
"--description",
"test note",
"--body",
"Quick scratch note for e2e validation",
"--skip-extraction",
])
.output()
.expect("remember must run");
assert!(
output.status.success(),
"remember failed: status={:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "list", "--type", "note", "--json"])
.output()
.expect("list must run");
assert!(
output.status.success(),
"list failed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("list output must be valid JSON");
let items = json["items"]
.as_array()
.expect("list response must contain `items` array");
assert_eq!(items.len(), 1, "expected exactly 1 note, got {items:?}");
assert_eq!(items[0]["type"], "note");
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "recall", "scratch", "--json"])
.output()
.expect("recall must run");
assert!(
output.status.success(),
"recall failed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("recall output must be valid JSON");
let results = json["results"]
.as_array()
.expect("recall response must contain `results` array");
assert!(
!results.is_empty(),
"recall must return at least one match for 'scratch', got: {results:?}"
);
}
#[test]
#[serial]
fn v009_invalid_type_rejected() {
let tmp = TempDir::new().expect("TempDir must be created");
let db_path = tmp.path().join("test.sqlite");
sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.env("SQLITE_GRAPHRAG_NAMESPACE", "global")
.args([
"--skip-memory-guard",
"remember",
"--name",
"x",
"--type",
"invalid_type_xyz",
"--description",
"t",
"--body",
"t",
])
.output()
.expect("remember must run");
assert!(
!output.status.success(),
"remember must reject invalid type 'invalid_type_xyz'"
);
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
assert!(
stderr.contains("invalid") || stderr.contains("type") || stderr.contains("possible values"),
"stderr should mention type rejection, got: {stderr}"
);
}
#[test]
#[serial]
fn migrate_rehash_is_noop_on_healthy_db() {
let (_tmp, db_path) = init_isolated_db();
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate", "--rehash"])
.output()
.expect("migrate --rehash must run");
assert!(
output.status.success(),
"migrate --rehash must succeed on a healthy DB. stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("stdout must be valid JSON");
assert_eq!(
json["status"], "ok_no_changes",
"healthy DB must report ok_no_changes, got: {stdout}"
);
assert_eq!(json["rewritten"].as_array().unwrap().len(), 0);
assert_eq!(json["inspected"], 13);
assert_eq!(json["schema_version"], 13);
}
#[test]
#[serial]
fn migrate_rehash_fixes_corrupted_checksum() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
conn.execute_batch(
"UPDATE refinery_schema_history SET checksum = '999999999999' WHERE version = 1",
)
.expect("corrupt V001 checksum");
drop(conn);
let bad = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate"])
.output()
.expect("migrate must run");
assert!(
!bad.status.success(),
"migrate must fail on a corrupted checksum, got: {:?}",
bad.status
);
let good = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate", "--rehash"])
.output()
.expect("migrate --rehash must run");
assert!(
good.status.success(),
"migrate --rehash must succeed. stderr={}",
String::from_utf8_lossy(&good.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&good.stdout).expect("JSON");
assert_eq!(json["status"], "ok_rewritten");
assert_eq!(json["rewritten"].as_array().unwrap().len(), 1);
assert_eq!(json["rewritten"][0]["version"], 1);
assert_eq!(json["rewritten"][0]["name"], "init");
assert_eq!(json["rewritten"][0]["old_checksum"], "999999999999");
let after = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate"])
.output()
.expect("migrate must run");
assert!(
after.status.success(),
"migrate must succeed after rehash. stderr={}",
String::from_utf8_lossy(&after.stderr)
);
}
#[test]
#[serial]
fn migrate_to_llm_only_reports_no_vec_tables_on_fresh_db() {
let (_tmp, db_path) = init_isolated_db();
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args([
"--skip-memory-guard",
"migrate",
"--to-llm-only",
"--drop-vec-tables",
])
.output()
.expect("migrate --to-llm-only must run");
assert!(
output.status.success(),
"migrate --to-llm-only must succeed on a fresh v1.0.76 DB. stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).expect("JSON");
assert_eq!(json["status"], "ok");
assert_eq!(json["schema_version"], 13);
assert_eq!(json["v013_applied"], true);
assert_eq!(
json["vec_tables_were_present"], false,
"fresh v1.0.76 DBs must not have vec0 virtual tables"
);
assert_eq!(json["rehashed"].as_array().unwrap().len(), 0);
}
#[test]
#[serial]
fn migrate_to_llm_only_requires_drop_vec_tables_safety_guard() {
let (_tmp, db_path) = init_isolated_db();
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate", "--to-llm-only"])
.output()
.expect("migrate --to-llm-only must run");
assert!(
!output.status.success(),
"migrate --to-llm-only without --drop-vec-tables must refuse to run"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("JSON");
assert_eq!(json["code"], 1, "validation error code 1 expected");
let msg = json["message"].as_str().unwrap_or("").to_string();
assert!(
msg.contains("--drop-vec-tables"),
"error message must mention --drop-vec-tables, got: {msg}"
);
}
#[test]
#[serial]
fn migrate_rehash_fixes_null_applied_on() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
conn.execute_batch("UPDATE refinery_schema_history SET applied_on = NULL")
.expect("nullify applied_on");
drop(conn);
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate", "--rehash"])
.output()
.expect("migrate --rehash must run");
assert!(
output.status.success(),
"migrate --rehash must succeed on DB with NULL applied_on. stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert!(
json["null_rows_fixed"].as_u64().unwrap_or(0) > 0,
"must report null_rows_fixed > 0, got: {}",
json["null_rows_fixed"]
);
let after = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args(["--skip-memory-guard", "migrate"])
.output()
.expect("migrate must run");
assert!(
after.status.success(),
"migrate must succeed after rehash fixed NULLs. stderr={}",
String::from_utf8_lossy(&after.stderr)
);
let conn = conn_ro(&db_path);
let null_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM refinery_schema_history WHERE applied_on IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(null_count, 0, "no NULL applied_on rows must remain");
}
#[test]
#[serial]
fn migrate_to_llm_only_fixes_null_applied_on() {
let (_tmp, db_path) = init_isolated_db();
let conn = conn_ro(&db_path);
conn.execute_batch("UPDATE refinery_schema_history SET applied_on = NULL")
.expect("nullify applied_on");
drop(conn);
let output = sgr_cmd()
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.args([
"--skip-memory-guard",
"migrate",
"--to-llm-only",
"--drop-vec-tables",
])
.output()
.expect("migrate --to-llm-only must run");
assert!(
output.status.success(),
"migrate --to-llm-only must succeed with NULL applied_on. stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert!(
json["null_rows_fixed"].as_u64().unwrap_or(0) > 0,
"must report null_rows_fixed > 0, got: {}",
json["null_rows_fixed"]
);
assert_eq!(json["status"], "ok");
}