use super::{MemoryError, MemoryKey, MemoryLimits, MemoryStore};
use rusqlite::{Connection, params};
use std::{sync::Barrier, thread};
use tempfile::TempDir;
fn store() -> (TempDir, MemoryStore) {
let directory = tempfile::tempdir().unwrap();
let store = MemoryStore::new(directory.path().join("memory/v1.sqlite3"));
(directory, store)
}
fn tiny_limits() -> MemoryLimits {
MemoryLimits {
content_bytes: 32,
records: 4,
total_content_bytes: 64,
database_bytes: 4 * 1_024 * 1_024,
scan_results: 2,
query_bytes: 16,
probation_duration_ms: 10,
}
}
#[test]
fn enforces_exact_ascii_and_unicode_byte_bounds() {
let directory = tempfile::tempdir().unwrap();
let limits = MemoryLimits {
content_bytes: 8,
total_content_bytes: 32,
query_bytes: 8,
..tiny_limits()
};
let store = MemoryStore::with_limits(directory.path().join("memory.sqlite3"), limits);
store.put("12345678", None, 0).unwrap();
assert!(matches!(
store.put("123456789", None, 0),
Err(MemoryError::ContentTooLarge { maximum_bytes: 8 })
));
store.put("éééé", None, 0).unwrap();
assert!(matches!(
store.put("ééééé", None, 0),
Err(MemoryError::ContentTooLarge { maximum_bytes: 8 })
));
store.scan("12345678", 1, 0).unwrap();
store.scan("éééé", 1, 0).unwrap();
assert!(matches!(
store.scan("123456789", 1, 0),
Err(MemoryError::QueryTooLarge { maximum_bytes: 8 })
));
assert!(matches!(
store.scan("ééééé", 1, 0),
Err(MemoryError::QueryTooLarge { maximum_bytes: 8 })
));
}
#[test]
fn normalized_identity_deduplicates_case_and_whitespace() {
let (_directory, store) = store();
store.put("Remember SQLite", None, 0).unwrap();
assert!(matches!(
store.put(" remember sqlite \n", None, 0),
Err(MemoryError::Duplicate)
));
}
#[test]
fn replacement_preserves_id_checks_version_and_adjusts_accounting() {
let directory = tempfile::tempdir().unwrap();
let limits = MemoryLimits {
content_bytes: 10,
total_content_bytes: 10,
..tiny_limits()
};
let store = MemoryStore::with_limits(directory.path().join("memory.sqlite3"), limits);
let original = store.put("123456", None, 1).unwrap();
store.scan("123456", 1, 2).unwrap();
store.read(&[original.key.id], 3).unwrap();
let replacement = store.put("1234", Some(original.key), 4).unwrap();
assert_eq!(replacement.key.id, original.key.id);
assert_eq!(replacement.key.version, original.key.version + 1);
assert_eq!(replacement.created_at_ms, original.created_at_ms);
assert_eq!(replacement.updated_at_ms, 4);
assert_eq!(replacement.scan_count, 0);
assert_eq!(replacement.use_count, 0);
assert_eq!(replacement.last_scanned_at_ms, None);
assert_eq!(replacement.last_used_at_ms, None);
assert_eq!(replacement.probation_until_ms, Some(14));
store.put("abcdef", None, 4).unwrap();
assert!(matches!(
store.put("other", Some(original.key), 5),
Err(MemoryError::Conflict)
));
assert!(matches!(
store.put("toolarge", None, 5),
Err(MemoryError::ContentCapacity { maximum_bytes: 10 })
));
}
#[test]
fn record_capacity_is_derived_from_live_rows() {
let directory = tempfile::tempdir().unwrap();
let limits = MemoryLimits {
records: 2,
..tiny_limits()
};
let store = MemoryStore::with_limits(directory.path().join("memory.sqlite3"), limits);
let first = store.put("one", None, 0).unwrap();
store.put("two", None, 0).unwrap();
assert!(matches!(
store.put("three", None, 0),
Err(MemoryError::RecordCapacity { maximum: 2 })
));
store.delete(first.key, 0).unwrap();
store.put("three", None, 0).unwrap();
assert_eq!(store.list(0).unwrap().len(), 2);
}
#[test]
fn delete_requires_the_current_version() {
let (_directory, store) = store();
let record = store.put("delete me", None, 0).unwrap();
let stale = MemoryKey {
id: record.key.id,
version: record.key.version + 1,
};
assert!(matches!(store.delete(stale, 0), Err(MemoryError::Conflict)));
store.delete(record.key, 0).unwrap();
assert!(store.list(0).unwrap().is_empty());
assert!(matches!(
store.delete(record.key, 0),
Err(MemoryError::NotFound)
));
}
#[test]
fn probation_prunes_at_the_exact_deadline() {
let directory = tempfile::tempdir().unwrap();
let store = MemoryStore::with_limits(directory.path().join("memory.sqlite3"), tiny_limits());
store.put("expires", None, 100).unwrap();
assert_eq!(store.list(109).unwrap().len(), 1);
assert!(store.list(110).unwrap().is_empty());
}
#[test]
fn pre_expiry_read_clears_probation() {
let directory = tempfile::tempdir().unwrap();
let store = MemoryStore::with_limits(directory.path().join("memory.sqlite3"), tiny_limits());
let record = store.put("keep this", None, 100).unwrap();
let read = store.read(&[record.key.id], 109).unwrap();
assert_eq!(read[0].probation_until_ms, None);
assert_eq!(read[0].use_count, 1);
assert_eq!(store.list(10_000).unwrap().len(), 1);
}
#[test]
fn scan_updates_only_returned_rows() {
let (_directory, store) = store();
let first = store.put("rust sqlite concise", None, 0).unwrap();
let second = store
.put("rust sqlite verbose padding words", None, 0)
.unwrap();
let unrelated = store.put("python network", None, 0).unwrap();
let scan = store.scan("rust sqlite", 1, 1).unwrap();
assert!(!scan.abstained);
assert_eq!(scan.candidates.len(), 1);
let records = store.list(1).unwrap();
let scanned = records
.iter()
.find(|record| record.key.id == scan.candidates[0].key.id)
.unwrap();
assert_eq!(scanned.scan_count, 1);
assert_eq!(scanned.last_scanned_at_ms, Some(1));
for id in [first.key.id, second.key.id, unrelated.key.id] {
let record = records.iter().find(|record| record.key.id == id).unwrap();
if id != scanned.key.id {
assert_eq!(record.scan_count, 0);
}
}
let abstained = store.scan("no-overlap", 2, 2).unwrap();
assert!(abstained.abstained);
assert!(abstained.candidates.is_empty());
assert_eq!(
store
.list(2)
.unwrap()
.iter()
.map(|record| record.scan_count)
.sum::<u64>(),
1
);
}
#[test]
fn scan_clamps_results_to_the_production_limit() {
let (_directory, store) = store();
for index in 0..6 {
store.put(&format!("shared term {index}"), None, 0).unwrap();
}
let scan = store.scan("shared", usize::MAX, 1).unwrap();
assert_eq!(scan.candidates.len(), 5);
}
#[test]
fn read_deduplicates_ids_and_updates_use_telemetry_once() {
let (_directory, store) = store();
let first = store.put("first", None, 0).unwrap();
let second = store.put("second", None, 0).unwrap();
let records = store
.read(&[first.key.id, first.key.id, 999, second.key.id], 5)
.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].key.id, first.key.id);
assert_eq!(records[1].key.id, second.key.id);
assert!(records.iter().all(|record| record.use_count == 1));
assert!(
records
.iter()
.all(|record| record.last_used_at_ms == Some(5))
);
let listed = store.list(5).unwrap();
assert!(listed.iter().all(|record| record.use_count == 1));
}
#[test]
fn concurrent_writers_preserve_every_record() {
let (_directory, store) = store();
let barrier = std::sync::Arc::new(Barrier::new(8));
let threads = (0..8)
.map(|index| {
let store = store.clone();
let barrier = barrier.clone();
thread::spawn(move || {
barrier.wait();
store.put(&format!("concurrent record {index}"), None, 0)
})
})
.collect::<Vec<_>>();
for thread in threads {
thread.join().unwrap().unwrap();
}
assert_eq!(store.list(0).unwrap().len(), 8);
}
#[test]
fn rejects_secrets_without_exposing_them_in_the_error() {
let (_directory, store) = store();
let secret = "password=hunter2";
let error = store.put(secret, None, 0).unwrap_err();
assert!(matches!(error, MemoryError::SecretRejected));
assert!(!error.to_string().contains("hunter2"));
assert!(store.list(0).unwrap().is_empty());
}
#[test]
fn suppresses_unsafe_legacy_rows_but_keeps_them_deletable() {
let (_directory, store) = store();
store.list(0).unwrap();
let connection = store.open().unwrap();
connection
.execute(
"INSERT INTO memories (
content, normalized_identity, created_at_ms, updated_at_ms,
scan_count, use_count, version
) VALUES (?1, 'legacy', 0, 0, 0, 0, 1)",
["password=hunter2"],
)
.unwrap();
let id = connection.last_insert_rowid();
drop(connection);
assert!(store.scan("hunter2", 1, 0).unwrap().abstained);
assert!(store.read(&[id], 0).unwrap().is_empty());
assert!(store.list(0).unwrap().is_empty());
store.delete(MemoryKey { id, version: 1 }, 0).unwrap();
}
#[test]
fn database_uses_delete_journaling_and_the_page_limit() {
let (_directory, store) = store();
store.put("integrity", None, 0).unwrap();
let connection = store.open().unwrap();
let journal_mode: String = connection
.query_row("PRAGMA journal_mode", [], |row| row.get(0))
.unwrap();
let page_size: i64 = connection
.query_row("PRAGMA page_size", [], |row| row.get(0))
.unwrap();
let maximum_pages: i64 = connection
.query_row("PRAGMA max_page_count", [], |row| row.get(0))
.unwrap();
let integrity: String = connection
.query_row("PRAGMA integrity_check", [], |row| row.get(0))
.unwrap();
assert_eq!(journal_mode, "delete");
assert_eq!(page_size, 4 * 1_024);
assert_eq!(maximum_pages * page_size, 4 * 1_024 * 1_024);
assert_eq!(integrity, "ok");
}
#[test]
fn newer_database_schema_versions_are_rejected_without_relabeling_them() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("memory.sqlite3");
let connection = Connection::open(&path).unwrap();
connection.pragma_update(None, "user_version", 2).unwrap();
drop(connection);
let store = MemoryStore::new(path.clone());
assert!(matches!(
store.list(0),
Err(MemoryError::UnsupportedSchemaVersion {
found: 2,
supported: 1
})
));
let connection = Connection::open(path).unwrap();
let schema_version: i64 = connection
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(schema_version, 2);
}
#[test]
fn stores_opening_the_same_global_path_share_the_corpus() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("global/memory/v1.sqlite3");
let first = MemoryStore::new(path.clone());
let second = MemoryStore::new(path);
let record = first.put("shared globally", None, 0).unwrap();
assert_eq!(second.list(0).unwrap(), vec![record]);
}
#[test]
fn replacement_of_a_legacy_row_does_not_require_reading_its_content() {
let (_directory, store) = store();
store.list(0).unwrap();
let connection = Connection::open(store.path.as_path()).unwrap();
connection
.execute(
"INSERT INTO memories (
content, normalized_identity, created_at_ms, updated_at_ms,
scan_count, use_count, version
) VALUES (?1, 'legacy unsafe', 0, 0, 0, 0, 1)",
params!["token=abcdefghijklmnop"],
)
.unwrap();
let id = connection.last_insert_rowid();
drop(connection);
let replacement = store
.put("safe replacement", Some(MemoryKey { id, version: 1 }), 1)
.unwrap();
assert_eq!(replacement.key, MemoryKey { id, version: 2 });
assert_eq!(store.list(1).unwrap(), [replacement]);
}
#[cfg(unix)]
#[test]
fn creates_a_private_database_directory() {
use std::os::unix::fs::PermissionsExt;
let directory = tempfile::tempdir().unwrap();
let parent = directory.path().join("memory");
let store = MemoryStore::new(parent.join("v1.sqlite3"));
store.list(0).unwrap();
let mode = std::fs::metadata(parent).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700);
}