use chrono::{TimeZone, Utc};
use cortex_core::MemoryId;
use cortex_store::migrate::apply_pending;
use cortex_store::repo::{EmbedRecord, EmbeddingRepo, EMBEDDING_ENCRYPTION_KIND_NONE};
use cortex_store::Pool;
use rusqlite::{params, Connection};
fn test_pool() -> Pool {
let pool = Connection::open_in_memory().expect("open in-memory sqlite");
apply_pending(&pool).expect("apply migrations");
pool
}
fn at(second: u32) -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, second).unwrap()
}
fn mem(id: &str) -> MemoryId {
id.parse().expect("valid memory id")
}
fn record(id: &str, backend: &str, vector: Vec<f32>, second: u32) -> EmbedRecord {
EmbedRecord::new(mem(id), backend, vector, at(second)).expect("valid record")
}
fn seed_memory(pool: &Pool, id: &str) {
pool.execute(
"INSERT OR IGNORE INTO memories (
id, memory_type, status, claim, source_episodes_json, source_events_json,
domains_json, salience_json, confidence, authority, applies_when_json,
does_not_apply_when_json, created_at, updated_at
) VALUES (?1, ?2, 'candidate', ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13);",
params![
id,
"semantic",
"test claim",
"[]",
r#"["evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"]"#,
r#"["store"]"#,
r#"{"score":0.5}"#,
0.8,
"candidate",
"{}",
"{}",
at(0).to_rfc3339(),
at(0).to_rfc3339(),
],
)
.expect("seed parent memory row");
}
#[test]
fn migration_007_creates_memory_embeddings_table() {
let pool = test_pool();
let names: Vec<String> = pool
.prepare("SELECT name FROM _migrations WHERE name = ?1;")
.unwrap()
.query_map(["007_embeddings"], |row| row.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(names, vec!["007_embeddings".to_string()]);
let columns: Vec<String> = pool
.prepare("PRAGMA table_info(memory_embeddings);")
.unwrap()
.query_map([], |row| row.get::<_, String>(1))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(
columns,
vec![
"memory_id",
"backend_id",
"dim",
"vector_blob",
"encryption_kind",
"encryption_key_id",
"computed_at",
]
);
let indexes: Vec<String> = pool
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'memory_embeddings' ORDER BY name;")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert!(
indexes.iter().any(|i| i == "idx_memory_embeddings_backend"),
"expected backend index, got {indexes:?}"
);
}
#[test]
fn migration_007_is_idempotent() {
let pool = test_pool();
let applied = apply_pending(&pool).expect("re-apply migrations");
assert_eq!(
applied, 0,
"migrations must be idempotent after first apply"
);
let count: i64 = pool
.query_row(
"SELECT COUNT(*) FROM _migrations WHERE name = '007_embeddings';",
[],
|row| row.get(0),
)
.expect("count migration rows");
assert_eq!(count, 1);
}
#[test]
fn embedding_repo_write_read_round_trip() {
let pool = test_pool();
seed_memory(&pool, "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV");
let repo = EmbeddingRepo::new(&pool);
let rec = record(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"stub:v1",
vec![0.1, -0.2, 0.3, 0.4, -0.5],
7,
);
repo.write(&rec).expect("write embedding");
let fetched = repo
.read(&rec.memory_id, &rec.backend_id)
.expect("read embedding")
.expect("row present after write");
assert_eq!(fetched, rec);
let encryption_kind: String = pool
.query_row(
"SELECT encryption_kind FROM memory_embeddings WHERE memory_id = ?1 AND backend_id = ?2;",
[rec.memory_id.to_string(), rec.backend_id.clone()],
|row| row.get(0),
)
.expect("read encryption_kind");
assert_eq!(encryption_kind, EMBEDDING_ENCRYPTION_KIND_NONE);
let absent = repo
.read(&mem("mem_01ARZ3NDEKTSV4RRFFQ69G5FAW"), "stub:v1")
.expect("read absent");
assert!(absent.is_none());
let rec_updated = EmbedRecord::new(
rec.memory_id,
&rec.backend_id,
vec![1.0, 2.0, 3.0, 4.0, 5.0],
at(9),
)
.expect("valid updated record");
repo.write(&rec_updated).expect("upsert embedding");
let fetched_updated = repo
.read(&rec.memory_id, &rec.backend_id)
.expect("read after upsert")
.expect("row present after upsert");
assert_eq!(fetched_updated, rec_updated);
let count: i64 = pool
.query_row(
"SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = ?1;",
[rec.memory_id.to_string()],
|row| row.get(0),
)
.expect("count rows");
assert_eq!(count, 1, "upsert must not duplicate rows");
}
#[test]
fn embedding_repo_list_by_backend_returns_records() {
let pool = test_pool();
seed_memory(&pool, "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV");
seed_memory(&pool, "mem_01ARZ3NDEKTSV4RRFFQ69G5FAW");
let repo = EmbeddingRepo::new(&pool);
let stub_a = record(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"stub:v1",
vec![0.1, 0.2, 0.3],
1,
);
let stub_b = record(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"stub:v1",
vec![-0.1, -0.2, -0.3],
2,
);
let other_backend = record(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"onnx:minilm-l6",
vec![0.4; 6],
3,
);
repo.write(&stub_a).expect("write stub a");
repo.write(&stub_b).expect("write stub b");
repo.write(&other_backend).expect("write other backend");
let stub_listing = repo.list_by_backend("stub:v1").expect("list by stub:v1");
assert_eq!(stub_listing.len(), 2);
assert_eq!(stub_listing[0], stub_a);
assert_eq!(stub_listing[1], stub_b);
let other_listing = repo
.list_by_backend("onnx:minilm-l6")
.expect("list by onnx:minilm-l6");
assert_eq!(other_listing, vec![other_backend]);
let empty = repo
.list_by_backend("never-registered")
.expect("list by unknown backend");
assert!(empty.is_empty());
}
#[test]
fn embedding_repo_delete_removes_row() {
let pool = test_pool();
seed_memory(&pool, "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV");
let repo = EmbeddingRepo::new(&pool);
let rec = record(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"stub:v1",
vec![0.5, 0.5, 0.5, 0.5],
4,
);
repo.write(&rec).expect("write embedding");
repo.delete(&rec.memory_id, &rec.backend_id)
.expect("delete embedding");
let after_delete = repo
.read(&rec.memory_id, &rec.backend_id)
.expect("read after delete");
assert!(after_delete.is_none());
repo.delete(&rec.memory_id, &rec.backend_id)
.expect("re-delete is a no-op");
let other_backend = record(
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"onnx:minilm-l6",
vec![0.1; 8],
5,
);
repo.write(&other_backend).expect("write other backend");
repo.delete(&other_backend.memory_id, "stub:v1")
.expect("delete unrelated row");
let survived = repo
.read(&other_backend.memory_id, "onnx:minilm-l6")
.expect("read survived row")
.expect("survived row present");
assert_eq!(survived, other_backend);
}