#![cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
use rusqlite::Connection;
use solo_core::{
Confidence, EncodingContext, Episode, Embedding, EmbeddingDtype, MemoryId, Result, Tier,
VectorIndex,
};
use std::path::Path;
pub fn open_test_db() -> (Connection, tempfile::TempDir) {
let tmp = tempfile::TempDir::new().expect("tempdir");
let path = tmp.path().join("test.db");
let mut conn = Connection::open(&path).expect("open db");
conn.execute_batch(
"PRAGMA journal_mode = wal;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;",
)
.expect("startup pragmas");
crate::migration::run_migrations(&mut conn).expect("migrations");
(conn, tmp)
}
pub fn open_test_db_at(path: &Path) -> Connection {
let mut conn = Connection::open(path).expect("open db");
conn.execute_batch(
"PRAGMA journal_mode = wal;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;",
)
.expect("startup pragmas");
crate::migration::run_migrations(&mut conn).expect("migrations");
conn
}
pub fn fixture_episode(content: &str) -> Episode {
Episode {
memory_id: MemoryId::new(),
ts_ms: chrono::Utc::now().timestamp_millis(),
source_type: "user_message".into(),
source_id: None,
content: content.into(),
encoding_context: EncodingContext::default(),
provenance: None,
confidence: Confidence::new(0.9).unwrap(),
strength: 0.5,
salience: 0.5,
tier: Tier::Hot,
}
}
pub fn fixture_embedding(dim: usize) -> Embedding {
Embedding {
dtype: EmbeddingDtype::F32,
dim,
data: vec![0u8; dim * 4],
}
}
pub struct StubVectorIndex {
state: Mutex<StubState>,
dim: usize,
add_sleep: Mutex<Option<std::time::Duration>>,
save_fails: Mutex<bool>,
}
struct StubState {
entries: Vec<(i64, Vec<f32>)>,
add_calls: usize,
remove_calls: usize,
save_calls: usize,
}
impl StubVectorIndex {
pub fn new(dim: usize) -> Self {
Self {
state: Mutex::new(StubState {
entries: Vec::new(),
add_calls: 0,
remove_calls: 0,
save_calls: 0,
}),
dim,
add_sleep: Mutex::new(None),
save_fails: Mutex::new(false),
}
}
pub fn add_count(&self) -> usize {
self.state.lock().add_calls
}
pub fn remove_count(&self) -> usize {
self.state.lock().remove_calls
}
pub fn save_count(&self) -> usize {
self.state.lock().save_calls
}
pub fn last_added(&self) -> Option<(i64, Vec<f32>)> {
self.state.lock().entries.last().cloned()
}
pub fn entries(&self) -> Vec<(i64, Vec<f32>)> {
self.state.lock().entries.clone()
}
pub fn set_add_sleep(&self, dur: Option<std::time::Duration>) {
*self.add_sleep.lock() = dur;
}
pub fn set_save_fails(&self, fail: bool) {
*self.save_fails.lock() = fail;
}
}
impl VectorIndex for StubVectorIndex {
fn add(&self, rowid: i64, embedding: &[f32]) -> Result<()> {
if let Some(dur) = *self.add_sleep.lock() {
std::thread::sleep(dur);
}
let mut s = self.state.lock();
s.add_calls += 1;
s.entries.push((rowid, embedding.to_vec()));
Ok(())
}
fn remove(&self, rowid: i64) -> Result<()> {
let mut s = self.state.lock();
s.remove_calls += 1;
s.entries.retain(|(r, _)| *r != rowid);
Ok(())
}
fn search(&self, _query: &[f32], k: usize) -> Result<Vec<(i64, f32)>> {
let s = self.state.lock();
Ok(s.entries
.iter()
.take(k)
.map(|(r, _)| (*r, 0.0))
.collect())
}
fn save(&self, _path: &std::path::Path) -> Result<()> {
self.state.lock().save_calls += 1;
if *self.save_fails.lock() {
return Err(solo_core::Error::vector_index(
"stub configured to fail save",
));
}
Ok(())
}
fn len(&self) -> usize {
self.state.lock().entries.len()
}
fn dim(&self) -> usize {
self.dim
}
}