#![cfg(feature = "slow-tests")]
use assert_cmd::Command;
use rusqlite::Connection;
use serial_test::serial;
use tempfile::TempDir;
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");
Command::cargo_bin("sqlite-graphrag")
.expect("sqlite-graphrag binary not found")
.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_9_migrations_v001_to_v009() {
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(),
9,
"exactly 9 migrations must be applied, found: {versions:?}"
);
assert_eq!(
versions,
vec![1, 2, 3, 4, 5, 6, 7, 8, 9],
"expected versions V001-V009"
);
}
#[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_due_to_vec_conflict() {
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 — sqlite-vec conflicts with FTS5 AFTER UPDATE"
);
}
#[test]
#[serial]
fn vec_memories_dim_384_cosine() {
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 = 'vec_memories'",
[],
|row| row.get(0),
)
.expect("vec_memories must exist in sqlite_master");
assert!(
ddl.contains("float[384]"),
"vec_memories must declare float[384], DDL was: {ddl}"
);
assert!(
ddl.contains("distance_metric=cosine"),
"vec_memories must use distance_metric=cosine, DDL was: {ddl}"
);
}
#[test]
#[serial]
fn vec_memories_partition_keys_namespace_type() {
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 = 'vec_memories'",
[],
|row| row.get(0),
)
.expect("vec_memories must exist in sqlite_master");
let namespace_pk = ddl.contains("namespace") && ddl.to_lowercase().contains("partition key");
let type_pk = ddl.contains("type") && ddl.to_lowercase().contains("partition key");
assert!(
namespace_pk,
"vec_memories must declare 'namespace' as partition key, DDL: {ddl}"
);
assert!(
type_pk,
"vec_memories must declare 'type' as partition key, DDL: {ddl}"
);
}
#[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");
Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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_id",
"idx_relationships_target_id",
"idx_relationships_namespace_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_9() {
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, "9",
"schema_version in schema_meta must be '9' after V009"
);
}
#[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");
Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
let output = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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 = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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 = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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");
Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
let output = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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 = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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 = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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");
Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.env("SQLITE_GRAPHRAG_DB_PATH", &db_path)
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "init"])
.assert()
.success();
let output = Command::cargo_bin("sqlite-graphrag")
.expect("binary not found")
.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}"
);
}