#![allow(missing_docs)]
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::RwLock;
use chrono::Utc;
use crate::auth::TenantScope;
use crate::error::Error;
use super::bm25;
use super::hybrid;
use super::scoring::{STRENGTH_DECAY_RATE, ScoringWeights, composite_score, effective_strength};
use super::{Memory, MemoryEntry, MemoryQuery};
pub struct InMemoryStore {
entries: RwLock<HashMap<String, MemoryEntry>>,
scoring_weights: ScoringWeights,
}
impl InMemoryStore {
pub fn new() -> Self {
Self {
entries: RwLock::new(HashMap::new()),
scoring_weights: ScoringWeights::default(),
}
}
pub fn with_scoring_weights(mut self, weights: ScoringWeights) -> Self {
self.scoring_weights = weights;
self
}
}
impl Default for InMemoryStore {
fn default() -> Self {
Self::new()
}
}
impl Memory for InMemoryStore {
fn store(
&self,
scope: &TenantScope,
mut entry: MemoryEntry,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
entry.author_tenant_id = Some(scope.tenant_id.clone());
entry.author_user_id = scope.user_id.clone();
Box::pin(async move {
let mut entries = self
.entries
.write()
.map_err(|e| Error::Memory(format!("lock poisoned: {e}")))?;
entries.insert(entry.id.clone(), entry);
Ok(())
})
}
fn recall(
&self,
scope: &TenantScope,
query: MemoryQuery,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, Error>> + Send + '_>> {
let tenant_id = scope.tenant_id.clone();
Box::pin(async move {
let mut entries = self
.entries
.write()
.map_err(|e| Error::Memory(format!("lock poisoned: {e}")))?;
let mut results: Vec<MemoryEntry> = entries
.values()
.filter(|e| {
if e.author_tenant_id.as_deref().unwrap_or("") != tenant_id.as_str() {
return false;
}
if let Some(ref text) = query.text {
let lower_content = e.content.to_lowercase();
let lower_keywords: Vec<String> =
e.keywords.iter().map(|k| k.to_lowercase()).collect();
let has_match = text.to_lowercase().split_whitespace().any(|token| {
lower_content.contains(token)
|| lower_keywords.iter().any(|k| k.contains(token))
});
if !has_match {
return false;
}
}
if let Some(ref cat) = query.category
&& e.category != *cat
{
return false;
}
if !query.tags.is_empty() && !query.tags.iter().any(|t| e.tags.contains(t)) {
return false;
}
if let Some(ref agent) = query.agent {
if e.agent != *agent {
return false;
}
} else if let Some(ref prefix) = query.agent_prefix
&& !e.agent.starts_with(prefix.as_str())
{
return false;
}
if let Some(ref mt) = query.memory_type
&& e.memory_type != *mt
{
return false;
}
if let Some(min_s) = query.min_strength {
let now = Utc::now();
let eff = effective_strength(
e.strength,
e.last_accessed,
now,
STRENGTH_DECAY_RATE,
);
if eff < min_s {
return false;
}
}
if let Some(max_conf) = query.max_confidentiality
&& e.confidentiality > max_conf
{
return false;
}
true
})
.cloned()
.collect();
let now = Utc::now();
let query_tokens: Vec<String> = query
.text
.as_deref()
.map(|t| {
let mut seen = std::collections::HashSet::new();
t.to_lowercase()
.split_whitespace()
.filter(|tok| seen.insert(tok.to_string()))
.map(String::from)
.collect()
})
.unwrap_or_default();
let avgdl = if results.is_empty() {
1.0
} else {
let total_words: usize = results
.iter()
.map(|e| e.content.split_whitespace().count())
.sum();
(total_words as f64 / results.len() as f64).max(1.0)
};
let bm25_map: HashMap<String, f64> = results
.iter()
.map(|e| {
let score = bm25::bm25_score(
&e.content,
&e.keywords,
&query_tokens,
avgdl,
bm25::DEFAULT_K1,
bm25::DEFAULT_B,
);
(e.id.clone(), score)
})
.collect();
let relevance_map: HashMap<String, f64> = if let Some(ref q_emb) = query.query_embedding
{
let mut bm25_ranked: Vec<(&str, f64)> =
bm25_map.iter().map(|(id, &s)| (id.as_str(), s)).collect();
bm25_ranked
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut vector_ranked: Vec<(&str, f64)> = results
.iter()
.filter_map(|e| {
e.embedding
.as_ref()
.map(|emb| (e.id.as_str(), hybrid::cosine_similarity(emb, q_emb)))
})
.collect();
vector_ranked
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
if vector_ranked.is_empty() {
let max_bm25 = bm25_map
.values()
.copied()
.fold(f64::NEG_INFINITY, f64::max)
.max(1.0);
bm25_map
.iter()
.map(|(id, &s)| (id.clone(), s / max_bm25))
.collect()
} else {
let fused = hybrid::rrf_fuse(&bm25_ranked, &vector_ranked, 50);
let max_fused = fused
.iter()
.map(|(_, s)| *s)
.fold(f64::NEG_INFINITY, f64::max)
.max(f64::EPSILON);
fused
.into_iter()
.map(|(id, s)| (id, s / max_fused))
.collect()
}
} else {
let max_bm25 = bm25_map
.values()
.copied()
.fold(f64::NEG_INFINITY, f64::max)
.max(1.0);
bm25_map
.iter()
.map(|(id, &s)| (id.clone(), s / max_bm25))
.collect()
};
results.sort_by(|a, b| {
let relevance_a = relevance_map.get(&a.id).copied().unwrap_or(0.0);
let relevance_b = relevance_map.get(&b.id).copied().unwrap_or(0.0);
let eff_a =
effective_strength(a.strength, a.last_accessed, now, STRENGTH_DECAY_RATE);
let eff_b =
effective_strength(b.strength, b.last_accessed, now, STRENGTH_DECAY_RATE);
let score_a = composite_score(
&self.scoring_weights,
a.created_at,
now,
a.importance,
relevance_a,
eff_a,
);
let score_b = composite_score(
&self.scoring_weights,
b.created_at,
now,
b.importance,
relevance_b,
eff_b,
);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
if query.limit > 0 {
results.truncate(query.limit);
}
let top_ids: std::collections::HashSet<String> =
results.iter().map(|e| e.id.clone()).collect();
let mut to_expand = Vec::new();
let mut seen_expanded = std::collections::HashSet::new();
for entry in &results {
for related_id in &entry.related_ids {
if !top_ids.contains(related_id) && seen_expanded.insert(related_id.clone()) {
to_expand.push(related_id.clone());
}
}
}
let mut expanded_ids = std::collections::HashSet::new();
let min_s = query.min_strength.unwrap_or(0.0);
for related_id in &to_expand {
if let Some(related) = entries.get(related_id) {
if let Some(max_conf) = query.max_confidentiality
&& related.confidentiality > max_conf
{
continue;
}
let eff = effective_strength(
related.strength,
related.last_accessed,
now,
STRENGTH_DECAY_RATE,
);
if eff >= min_s {
expanded_ids.insert(related_id.clone());
results.push(related.clone());
}
}
}
if !expanded_ids.is_empty() {
let new_avgdl = if results.is_empty() {
1.0
} else {
let total_words: usize = results
.iter()
.map(|e| e.content.split_whitespace().count())
.sum();
(total_words as f64 / results.len() as f64).max(1.0)
};
let new_bm25_map: HashMap<String, f64> = results
.iter()
.filter(|e| !bm25_map.contains_key(&e.id))
.map(|e| {
let score = bm25::bm25_score(
&e.content,
&e.keywords,
&query_tokens,
new_avgdl,
bm25::DEFAULT_K1,
bm25::DEFAULT_B,
);
(e.id.clone(), score)
})
.collect();
let combined_max = bm25_map
.values()
.chain(new_bm25_map.values())
.copied()
.fold(f64::NEG_INFINITY, f64::max)
.max(1.0);
results.sort_by(|a, b| {
let rel_a = bm25_map
.get(&a.id)
.or_else(|| new_bm25_map.get(&a.id))
.copied()
.unwrap_or(0.0)
/ combined_max;
let rel_b = bm25_map
.get(&b.id)
.or_else(|| new_bm25_map.get(&b.id))
.copied()
.unwrap_or(0.0)
/ combined_max;
let eff_a =
effective_strength(a.strength, a.last_accessed, now, STRENGTH_DECAY_RATE);
let eff_b =
effective_strength(b.strength, b.last_accessed, now, STRENGTH_DECAY_RATE);
let score_a = composite_score(
&self.scoring_weights,
a.created_at,
now,
a.importance,
rel_a,
eff_a,
);
let score_b = composite_score(
&self.scoring_weights,
b.created_at,
now,
b.importance,
rel_b,
eff_b,
);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
if query.limit > 0 {
results.truncate(query.limit);
}
}
for r in &mut results {
if let Some(e) = entries.get_mut(&r.id) {
e.access_count += 1;
e.last_accessed = now;
e.strength = (e.strength + 0.2).min(1.0);
r.access_count = e.access_count;
r.last_accessed = now;
r.strength = e.strength;
}
}
Ok(results)
})
}
fn update(
&self,
scope: &TenantScope,
id: &str,
content: String,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
let id = id.to_string();
let tenant_id = scope.tenant_id.clone();
Box::pin(async move {
let mut entries = self
.entries
.write()
.map_err(|e| Error::Memory(format!("lock poisoned: {e}")))?;
match entries.get_mut(&id) {
Some(entry)
if entry.author_tenant_id.as_deref().unwrap_or("") == tenant_id.as_str() =>
{
entry.content = content;
entry.last_accessed = Utc::now();
Ok(())
}
Some(_) => {
Err(Error::Memory(format!("memory not found: {id}")))
}
None => Err(Error::Memory(format!("memory not found: {id}"))),
}
})
}
fn forget(
&self,
scope: &TenantScope,
id: &str,
) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + '_>> {
let id = id.to_string();
let tenant_id = scope.tenant_id.clone();
Box::pin(async move {
let mut entries = self
.entries
.write()
.map_err(|e| Error::Memory(format!("lock poisoned: {e}")))?;
let belongs = entries
.get(&id)
.map(|e| e.author_tenant_id.as_deref().unwrap_or("") == tenant_id.as_str())
.unwrap_or(false);
if belongs {
Ok(entries.remove(&id).is_some())
} else {
Ok(false)
}
})
}
fn add_link(
&self,
scope: &TenantScope,
id: &str,
related_id: &str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
let id = id.to_string();
let related_id = related_id.to_string();
let tenant_id = scope.tenant_id.clone();
Box::pin(async move {
let mut entries = self
.entries
.write()
.map_err(|e| Error::Memory(format!("lock poisoned: {e}")))?;
let id_ok = entries
.get(&id)
.map(|e| e.author_tenant_id.as_deref().unwrap_or("") == tenant_id.as_str())
.unwrap_or(false);
let rel_ok = entries
.get(&related_id)
.map(|e| e.author_tenant_id.as_deref().unwrap_or("") == tenant_id.as_str())
.unwrap_or(false);
if id_ok
&& let Some(entry) = entries.get_mut(&id)
&& !entry.related_ids.contains(&related_id)
{
entry.related_ids.push(related_id.clone());
}
if rel_ok
&& let Some(entry) = entries.get_mut(&related_id)
&& !entry.related_ids.contains(&id)
{
entry.related_ids.push(id);
}
Ok(())
})
}
fn prune(
&self,
scope: &TenantScope,
min_strength: f64,
min_age: chrono::Duration,
agent_prefix: Option<&str>,
) -> Pin<Box<dyn Future<Output = Result<usize, Error>> + Send + '_>> {
let owned_prefix = agent_prefix.map(String::from);
let tenant_id = scope.tenant_id.clone();
Box::pin(async move {
let mut entries = self
.entries
.write()
.map_err(|e| Error::Memory(format!("lock poisoned: {e}")))?;
let now = Utc::now();
let to_remove: Vec<String> = entries
.values()
.filter(|e| {
if e.author_tenant_id.as_deref().unwrap_or("") != tenant_id.as_str() {
return false;
}
if let Some(ref prefix) = owned_prefix
&& !e.agent.starts_with(prefix.as_str())
{
return false;
}
let eff =
effective_strength(e.strength, e.last_accessed, now, STRENGTH_DECAY_RATE);
eff < min_strength && now.signed_duration_since(e.created_at) > min_age
})
.map(|e| e.id.clone())
.collect();
let count = to_remove.len();
for id in to_remove {
entries.remove(&id);
}
Ok(count)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use super::super::{Confidentiality, MemoryType};
fn test_scope() -> TenantScope {
TenantScope::default()
}
fn make_entry(id: &str, agent: &str, content: &str, category: &str) -> MemoryEntry {
MemoryEntry {
id: id.into(),
agent: agent.into(),
content: content.into(),
category: category.into(),
tags: vec![],
created_at: Utc::now(),
last_accessed: Utc::now(),
access_count: 0,
importance: 5,
memory_type: MemoryType::default(),
keywords: vec![],
summary: None,
strength: 1.0,
related_ids: vec![],
source_ids: vec![],
embedding: None,
confidentiality: Confidentiality::default(),
author_user_id: None,
author_tenant_id: None,
}
}
fn make_entry_with_tags(
id: &str,
agent: &str,
content: &str,
category: &str,
tags: Vec<String>,
) -> MemoryEntry {
MemoryEntry {
id: id.into(),
agent: agent.into(),
content: content.into(),
category: category.into(),
tags,
created_at: Utc::now(),
last_accessed: Utc::now(),
access_count: 0,
importance: 5,
memory_type: MemoryType::default(),
keywords: vec![],
summary: None,
strength: 1.0,
related_ids: vec![],
source_ids: vec![],
embedding: None,
confidentiality: Confidentiality::default(),
author_user_id: None,
author_tenant_id: None,
}
}
#[tokio::test]
async fn store_and_recall() {
let store = InMemoryStore::new();
let entry = make_entry("m1", "agent1", "Rust is fast", "fact");
store.store(&test_scope(), entry).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].content, "Rust is fast");
}
#[tokio::test]
async fn recall_by_text() {
let store = InMemoryStore::new();
store
.store(&test_scope(), make_entry("m1", "a", "Rust is fast", "fact"))
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m2", "a", "Python is slow", "fact"),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn recall_by_category() {
let store = InMemoryStore::new();
store
.store(
&test_scope(),
make_entry("m1", "a", "remember this", "fact"),
)
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m2", "a", "I saw something", "observation"),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
category: Some("observation".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m2");
}
#[tokio::test]
async fn recall_by_tags() {
let store = InMemoryStore::new();
store
.store(
&test_scope(),
make_entry_with_tags(
"m1",
"a",
"Rust memory safety",
"fact",
vec!["rust".into(), "safety".into()],
),
)
.await
.unwrap();
store
.store(
&test_scope(),
make_entry_with_tags(
"m2",
"a",
"Go is garbage collected",
"fact",
vec!["go".into()],
),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
tags: vec!["rust".into()],
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn recall_by_agent() {
let store = InMemoryStore::new();
store
.store(
&test_scope(),
make_entry("m1", "researcher", "data point", "fact"),
)
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m2", "coder", "code snippet", "procedure"),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
agent: Some("researcher".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn recall_limit() {
let store = InMemoryStore::new();
for i in 0..10 {
store
.store(
&test_scope(),
make_entry(&format!("m{i}"), "a", &format!("entry {i}"), "fact"),
)
.await
.unwrap();
}
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 3,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn update_existing() {
let store = InMemoryStore::new();
store
.store(&test_scope(), make_entry("m1", "a", "original", "fact"))
.await
.unwrap();
store
.update(&test_scope(), "m1", "updated content".into())
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results[0].content, "updated content");
}
#[tokio::test]
async fn update_nonexistent() {
let store = InMemoryStore::new();
let err = store
.update(&test_scope(), "missing", "content".into())
.await
.unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[tokio::test]
async fn forget_existing() {
let store = InMemoryStore::new();
store
.store(&test_scope(), make_entry("m1", "a", "to delete", "fact"))
.await
.unwrap();
assert!(store.forget(&test_scope(), "m1").await.unwrap());
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
async fn forget_nonexistent() {
let store = InMemoryStore::new();
assert!(!store.forget(&test_scope(), "missing").await.unwrap());
}
#[test]
fn is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<InMemoryStore>();
}
#[tokio::test]
async fn recall_sorts_by_composite_score() {
let store = InMemoryStore::new();
let mut high_imp = make_entry("m1", "a", "high importance", "fact");
high_imp.importance = 10;
high_imp.created_at = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), high_imp).await.unwrap();
let mut low_imp = make_entry("m2", "a", "low importance", "fact");
low_imp.importance = 1;
low_imp.created_at = Utc::now();
store.store(&test_scope(), low_imp).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, "m1");
assert_eq!(results[1].id, "m2");
}
#[tokio::test]
async fn recall_recent_high_importance_first() {
let store = InMemoryStore::new();
let mut old_low = make_entry("m1", "a", "old low", "fact");
old_low.importance = 1;
old_low.created_at = Utc::now() - chrono::Duration::hours(1000);
store.store(&test_scope(), old_low).await.unwrap();
let mut recent_high = make_entry("m2", "a", "recent high", "fact");
recent_high.importance = 10;
recent_high.created_at = Utc::now();
store.store(&test_scope(), recent_high).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results[0].id, "m2");
}
#[tokio::test]
async fn recall_with_custom_weights() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 1.0,
gamma: 0.0,
delta: 0.0,
decay_rate: 0.01,
});
let mut low = make_entry("m1", "a", "recent but low", "fact");
low.importance = 1;
low.created_at = Utc::now();
store.store(&test_scope(), low).await.unwrap();
let mut high = make_entry("m2", "a", "old but high", "fact");
high.importance = 10;
high.created_at = Utc::now() - chrono::Duration::hours(1000);
store.store(&test_scope(), high).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results[0].id, "m2");
}
#[tokio::test]
async fn recall_text_query_affects_relevance() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
let mut e1 = make_entry("m1", "a", "Rust is fast", "fact");
e1.importance = 5;
store.store(&test_scope(), e1).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("Rust".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn recall_limit_zero_returns_all() {
let store = InMemoryStore::new();
for i in 0..5 {
store
.store(
&test_scope(),
make_entry(&format!("m{i}"), "a", &format!("entry {i}"), "fact"),
)
.await
.unwrap();
}
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 0,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 5);
}
#[tokio::test]
async fn recall_deduplicates_query_tokens() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
store
.store(&test_scope(), make_entry("m1", "a", "Rust is fast", "fact"))
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m2", "a", "Python is slow", "fact"),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust rust rust".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn relevance_score_differentiates_results() {
let store = InMemoryStore::new();
let mut entry_partial =
make_entry("m1", "agent1", "Rust is popular in the industry", "fact");
entry_partial.importance = 5;
let mut entry_full =
make_entry("m2", "agent1", "Rust is fast and safe for systems", "fact");
entry_full.importance = 5;
store.store(&test_scope(), entry_partial).await.unwrap();
store.store(&test_scope(), entry_full).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("Rust fast".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(
results[0].id, "m2",
"entry matching more query tokens should rank first"
);
}
#[tokio::test]
async fn recall_filters_by_memory_type() {
let store = InMemoryStore::new();
let mut episodic = make_entry("m1", "a", "episodic fact", "fact");
episodic.memory_type = MemoryType::Episodic;
store.store(&test_scope(), episodic).await.unwrap();
let mut semantic = make_entry("m2", "a", "semantic knowledge", "fact");
semantic.memory_type = MemoryType::Semantic;
store.store(&test_scope(), semantic).await.unwrap();
let mut reflection = make_entry("m3", "a", "reflection insight", "fact");
reflection.memory_type = MemoryType::Reflection;
store.store(&test_scope(), reflection).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
memory_type: Some(MemoryType::Semantic),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m2");
let results = store
.recall(
&test_scope(),
MemoryQuery {
memory_type: Some(MemoryType::Reflection),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m3");
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn recall_filters_by_min_strength() {
let store = InMemoryStore::new();
let mut strong = make_entry("m1", "a", "strong memory", "fact");
strong.strength = 0.9;
store.store(&test_scope(), strong).await.unwrap();
let mut weak = make_entry("m2", "a", "weak memory", "fact");
weak.strength = 0.05;
store.store(&test_scope(), weak).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
min_strength: Some(0.5),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn strength_reinforced_on_access() {
let store = InMemoryStore::new();
let mut entry = make_entry("m1", "a", "test", "fact");
entry.strength = 0.5;
store.store(&test_scope(), entry).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert!((results[0].strength - 0.7).abs() < f64::EPSILON);
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert!((results[0].strength - 0.9).abs() < f64::EPSILON);
}
#[tokio::test]
async fn strength_capped_at_one() {
let store = InMemoryStore::new();
let mut entry = make_entry("m1", "a", "test", "fact");
entry.strength = 0.95;
store.store(&test_scope(), entry).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert!((results[0].strength - 1.0).abs() < f64::EPSILON);
}
#[tokio::test]
async fn keywords_searched_during_recall() {
let store = InMemoryStore::new();
let mut entry = make_entry("m1", "a", "Rust is great", "fact");
entry.keywords = vec!["performance".into(), "speed".into()];
store.store(&test_scope(), entry).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("performance".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn add_link_bidirectional() {
let store = InMemoryStore::new();
store
.store(&test_scope(), make_entry("m1", "a", "first", "fact"))
.await
.unwrap();
store
.store(&test_scope(), make_entry("m2", "a", "second", "fact"))
.await
.unwrap();
store.add_link(&test_scope(), "m1", "m2").await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
let m1 = results.iter().find(|e| e.id == "m1").unwrap();
let m2 = results.iter().find(|e| e.id == "m2").unwrap();
assert!(m1.related_ids.contains(&"m2".to_string()));
assert!(m2.related_ids.contains(&"m1".to_string()));
}
#[tokio::test]
async fn add_link_idempotent() {
let store = InMemoryStore::new();
store
.store(&test_scope(), make_entry("m1", "a", "first", "fact"))
.await
.unwrap();
store
.store(&test_scope(), make_entry("m2", "a", "second", "fact"))
.await
.unwrap();
store.add_link(&test_scope(), "m1", "m2").await.unwrap();
store.add_link(&test_scope(), "m1", "m2").await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
let m1 = results.iter().find(|e| e.id == "m1").unwrap();
assert_eq!(
m1.related_ids.iter().filter(|id| *id == "m2").count(),
1,
"should not have duplicate links"
);
}
#[tokio::test]
async fn prune_removes_below_threshold() {
let store = InMemoryStore::new();
let mut strong = make_entry("m1", "a", "strong", "fact");
strong.strength = 0.8;
strong.created_at = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), strong).await.unwrap();
let mut weak = make_entry("m2", "a", "weak", "fact");
weak.strength = 0.05;
weak.created_at = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), weak).await.unwrap();
let pruned = store
.prune(&test_scope(), 0.1, chrono::Duration::hours(1), None)
.await
.unwrap();
assert_eq!(pruned, 1);
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn prune_respects_min_age() {
let store = InMemoryStore::new();
let mut weak_recent = make_entry("m1", "a", "weak recent", "fact");
weak_recent.strength = 0.01;
weak_recent.created_at = Utc::now(); store.store(&test_scope(), weak_recent).await.unwrap();
let pruned = store
.prune(&test_scope(), 0.1, chrono::Duration::hours(24), None)
.await
.unwrap();
assert_eq!(pruned, 0, "recent entry should not be pruned");
}
#[tokio::test]
async fn prune_uses_effective_strength_with_decay() {
let store = InMemoryStore::new();
let mut old_accessed = make_entry("m1", "a", "old accessed", "fact");
old_accessed.strength = 0.5;
old_accessed.created_at = Utc::now() - chrono::Duration::hours(30 * 24);
old_accessed.last_accessed = Utc::now() - chrono::Duration::hours(30 * 24);
store.store(&test_scope(), old_accessed).await.unwrap();
let mut recently_accessed = make_entry("m2", "a", "recently accessed", "fact");
recently_accessed.strength = 0.5;
recently_accessed.created_at = Utc::now() - chrono::Duration::hours(30 * 24);
recently_accessed.last_accessed = Utc::now();
store.store(&test_scope(), recently_accessed).await.unwrap();
let pruned = store
.prune(&test_scope(), 0.1, chrono::Duration::hours(24), None)
.await
.unwrap();
assert_eq!(pruned, 1, "old unaccessed entry should be pruned");
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m2");
}
#[tokio::test]
async fn prune_with_agent_prefix_only_removes_matching_agent() {
let store = InMemoryStore::new();
let mut weak_a = make_entry("m1", "agent_a", "weak from A", "fact");
weak_a.strength = 0.01;
weak_a.created_at = Utc::now() - chrono::Duration::hours(48);
weak_a.last_accessed = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), weak_a).await.unwrap();
let mut weak_b = make_entry("m2", "agent_b", "weak from B", "fact");
weak_b.strength = 0.01;
weak_b.created_at = Utc::now() - chrono::Duration::hours(48);
weak_b.last_accessed = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), weak_b).await.unwrap();
let pruned = store
.prune(
&test_scope(),
0.1,
chrono::Duration::hours(1),
Some("agent_a"),
)
.await
.unwrap();
assert_eq!(pruned, 1, "should only prune agent_a's entry");
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m2");
assert_eq!(results[0].agent, "agent_b");
}
#[tokio::test]
async fn prune_none_prefix_removes_all_matching() {
let store = InMemoryStore::new();
let mut weak_a = make_entry("m1", "agent_a", "weak from A", "fact");
weak_a.strength = 0.01;
weak_a.created_at = Utc::now() - chrono::Duration::hours(48);
weak_a.last_accessed = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), weak_a).await.unwrap();
let mut weak_b = make_entry("m2", "agent_b", "weak from B", "fact");
weak_b.strength = 0.01;
weak_b.created_at = Utc::now() - chrono::Duration::hours(48);
weak_b.last_accessed = Utc::now() - chrono::Duration::hours(48);
store.store(&test_scope(), weak_b).await.unwrap();
let pruned = store
.prune(&test_scope(), 0.1, chrono::Duration::hours(1), None)
.await
.unwrap();
assert_eq!(pruned, 2, "should prune all weak entries");
}
#[tokio::test]
async fn recall_bm25_ranks_better_than_naive_keyword() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
let e1 = make_entry("m1", "a", "Rust is a programming language", "fact");
store.store(&test_scope(), e1).await.unwrap();
let e2 = make_entry(
"m2",
"a",
"Rust has excellent performance and speed",
"fact",
);
store.store(&test_scope(), e2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("Rust performance".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(
results[0].id, "m2",
"BM25 should rank entry matching more query terms first"
);
}
#[tokio::test]
async fn recall_bm25_keyword_field_boosts_ranking() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
let e1 = make_entry("m1", "a", "optimization techniques for databases", "fact");
store.store(&test_scope(), e1).await.unwrap();
let mut e2 = make_entry("m2", "a", "optimization techniques for systems", "fact");
e2.keywords = vec!["optimization".into(), "databases".into()];
store.store(&test_scope(), e2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("optimization databases".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(
results[0].id, "m2",
"entry with keyword match should rank higher"
);
}
#[tokio::test]
async fn strength_affects_ranking() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 0.0,
delta: 1.0,
decay_rate: 0.01,
});
let mut weak = make_entry("m1", "a", "weak entry", "fact");
weak.strength = 0.2;
store.store(&test_scope(), weak).await.unwrap();
let mut strong = make_entry("m2", "a", "strong entry", "fact");
strong.strength = 0.9;
store.store(&test_scope(), strong).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(
results[0].id, "m2",
"stronger entry should rank first when delta=1.0"
);
}
#[tokio::test]
async fn hybrid_recall_cosine_boosts_semantic_match() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
let e1 = make_entry("m1", "a", "Rust is fast", "fact");
store.store(&test_scope(), e1).await.unwrap();
let mut e2 = make_entry(
"m2",
"a",
"Systems programming language with safety",
"fact",
);
e2.embedding = Some(vec![0.9, 0.1, 0.0]);
store.store(&test_scope(), e2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust".into()),
query_embedding: Some(vec![0.9, 0.1, 0.0]),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn hybrid_recall_fuses_bm25_and_vector() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
let mut e1 = make_entry("m1", "a", "Rust is fast and fast", "fact");
e1.embedding = Some(vec![0.0, 0.0, 1.0]); store.store(&test_scope(), e1).await.unwrap();
let mut e2 = make_entry("m2", "a", "Rust has zero-cost abstractions", "fact");
e2.embedding = Some(vec![0.95, 0.05, 0.0]); store.store(&test_scope(), e2).await.unwrap();
let mut e3 = make_entry("m3", "a", "Rust is a programming language", "fact");
e3.embedding = Some(vec![0.5, 0.5, 0.0]); store.store(&test_scope(), e3).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust fast".into()),
query_embedding: Some(vec![0.95, 0.05, 0.0]),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 3);
assert_eq!(
results[0].id, "m2",
"entry with highest cosine similarity should rank first in hybrid mode"
);
}
#[tokio::test]
async fn hybrid_recall_bm25_fallback_when_no_embeddings() {
let store = InMemoryStore::new().with_scoring_weights(ScoringWeights {
alpha: 0.0,
beta: 0.0,
gamma: 1.0,
delta: 0.0,
decay_rate: 0.01,
});
let e1 = make_entry("m1", "a", "Rust programming language", "fact");
store.store(&test_scope(), e1).await.unwrap();
let e2 = make_entry("m2", "a", "Rust performance and speed", "fact");
store.store(&test_scope(), e2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("Rust performance".into()),
query_embedding: Some(vec![0.5, 0.5, 0.0]),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, "m2");
}
#[tokio::test]
async fn recall_follows_related_ids_one_hop() {
let store = InMemoryStore::new();
let mut m1 = make_entry("m1", "a", "Rust is fast", "fact");
m1.related_ids = vec!["m2".into()];
store.store(&test_scope(), m1).await.unwrap();
let mut m2 = make_entry("m2", "a", "Memory safety guarantees", "fact");
m2.related_ids = vec!["m1".into()];
store.store(&test_scope(), m2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<&str> = results.iter().map(|e| e.id.as_str()).collect();
assert!(ids.contains(&"m1"), "direct match should be in results");
assert!(
ids.contains(&"m2"),
"linked entry should be surfaced via graph expansion"
);
}
#[tokio::test]
async fn recall_graph_expansion_respects_strength_threshold() {
let store = InMemoryStore::new();
let mut m1 = make_entry("m1", "a", "Rust is fast", "fact");
m1.related_ids = vec!["m2".into()];
store.store(&test_scope(), m1).await.unwrap();
let mut m2 = make_entry("m2", "a", "Weak linked memory", "fact");
m2.related_ids = vec!["m1".into()];
m2.strength = 0.01;
m2.last_accessed = Utc::now() - chrono::Duration::hours(720); store.store(&test_scope(), m2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust".into()),
min_strength: Some(0.1),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn recall_graph_expansion_does_not_duplicate() {
let store = InMemoryStore::new();
let mut m1 = make_entry("m1", "a", "Rust is fast", "fact");
m1.related_ids = vec!["m2".into()];
store.store(&test_scope(), m1).await.unwrap();
let mut m2 = make_entry("m2", "a", "Rust is safe", "fact");
m2.related_ids = vec!["m1".into()];
store.store(&test_scope(), m2).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("rust".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<&str> = results.iter().map(|e| e.id.as_str()).collect();
assert_eq!(
ids.iter().filter(|&&id| id == "m1").count(),
1,
"m1 should appear exactly once"
);
assert_eq!(
ids.iter().filter(|&&id| id == "m2").count(),
1,
"m2 should appear exactly once"
);
}
#[tokio::test]
async fn recall_agent_prefix_matches_sub_namespaces() {
let store = InMemoryStore::new();
store
.store(
&test_scope(),
make_entry("m1", "tg:123:assistant", "likes Rust", "fact"),
)
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m2", "tg:123:researcher", "loves coffee", "fact"),
)
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m3", "tg:456:assistant", "prefers Python", "fact"),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
agent_prefix: Some("tg:123".into()),
limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<&str> = results.iter().map(|e| e.id.as_str()).collect();
assert!(ids.contains(&"m1"));
assert!(ids.contains(&"m2"));
}
#[tokio::test]
async fn recall_agent_exact_takes_precedence_over_prefix() {
let store = InMemoryStore::new();
store
.store(
&test_scope(),
make_entry("m1", "tg:123:assistant", "from assistant", "fact"),
)
.await
.unwrap();
store
.store(
&test_scope(),
make_entry("m2", "tg:123:researcher", "from researcher", "fact"),
)
.await
.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
agent: Some("tg:123:assistant".into()),
agent_prefix: Some("tg:123".into()), limit: 10,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
}
#[tokio::test]
async fn recall_filters_by_max_confidentiality() {
use super::super::Confidentiality;
let store = InMemoryStore::new();
let mut public = make_entry("m1", "a", "public fact", "fact");
public.confidentiality = Confidentiality::Public;
store.store(&test_scope(), public).await.unwrap();
let mut internal = make_entry("m2", "a", "internal note", "fact");
internal.confidentiality = Confidentiality::Internal;
store.store(&test_scope(), internal).await.unwrap();
let mut confidential = make_entry("m3", "a", "private expense", "fact");
confidential.confidentiality = Confidentiality::Confidential;
store.store(&test_scope(), confidential).await.unwrap();
let mut restricted = make_entry("m4", "a", "api key", "fact");
restricted.confidentiality = Confidentiality::Restricted;
store.store(&test_scope(), restricted).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
max_confidentiality: Some(Confidentiality::Public),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "m1");
let results = store
.recall(
&test_scope(),
MemoryQuery {
max_confidentiality: None,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 4);
let results = store
.recall(
&test_scope(),
MemoryQuery {
max_confidentiality: Some(Confidentiality::Confidential),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|e| e.id != "m4"));
}
#[tokio::test]
async fn graph_expansion_respects_max_confidentiality() {
use super::super::Confidentiality;
let store = InMemoryStore::new();
let mut public = make_entry("m1", "a", "project update", "fact");
public.confidentiality = Confidentiality::Public;
public.related_ids = vec!["m2".into()];
public.keywords = vec!["project".into()];
store.store(&test_scope(), public).await.unwrap();
let mut confidential = make_entry("m2", "a", "private expense data", "fact");
confidential.confidentiality = Confidentiality::Confidential;
confidential.keywords = vec!["expense".into()];
store.store(&test_scope(), confidential).await.unwrap();
let results = store
.recall(
&test_scope(),
MemoryQuery {
text: Some("project".into()),
max_confidentiality: Some(Confidentiality::Public),
..Default::default()
},
)
.await
.unwrap();
assert!(
results.iter().all(|e| e.id != "m2"),
"graph expansion should not include Confidential entries when capped at Public"
);
assert!(results.iter().any(|e| e.id == "m1"));
}
#[tokio::test]
async fn recall_does_not_leak_across_tenants() {
let store = InMemoryStore::new();
let acme = TenantScope::new("acme");
let globex = TenantScope::new("globex");
store
.store(&acme, make_entry("a1", "agent", "acme-secret", "fact"))
.await
.unwrap();
store
.store(&globex, make_entry("g1", "agent", "globex-secret", "fact"))
.await
.unwrap();
let acme_results = store
.recall(
&acme,
MemoryQuery {
agent: Some("agent".into()),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(acme_results.len(), 1);
assert_eq!(acme_results[0].id, "a1");
let globex_results = store
.recall(
&globex,
MemoryQuery {
agent: Some("agent".into()),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(globex_results.len(), 1);
assert_eq!(globex_results[0].id, "g1");
}
#[tokio::test]
async fn forget_does_not_delete_other_tenant() {
let store = InMemoryStore::new();
let acme = TenantScope::new("acme");
let globex = TenantScope::new("globex");
store
.store(&acme, make_entry("a1", "agent", "x", "fact"))
.await
.unwrap();
store
.store(&globex, make_entry("g1", "agent", "y", "fact"))
.await
.unwrap();
let removed = store.forget(&globex, "a1").await.unwrap();
assert!(!removed);
let acme_results = store
.recall(
&acme,
MemoryQuery {
agent: Some("agent".into()),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(acme_results.len(), 1);
}
#[tokio::test]
async fn update_does_not_modify_other_tenant() {
let store = InMemoryStore::new();
let acme = TenantScope::new("acme");
let globex = TenantScope::new("globex");
store
.store(&acme, make_entry("a1", "agent", "original", "fact"))
.await
.unwrap();
let err = store
.update(&globex, "a1", "tampered".into())
.await
.unwrap_err();
assert!(err.to_string().contains("memory not found"), "got: {err}");
let results = store
.recall(
&acme,
MemoryQuery {
agent: Some("agent".into()),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].content, "original");
}
#[tokio::test]
async fn store_under_scope_populates_author_tenant_id() {
let store = InMemoryStore::new();
let scope = TenantScope::new("acme").with_user("u-42");
store
.store(&scope, make_entry("s1", "agent", "x", "fact"))
.await
.unwrap();
let results = store
.recall(
&scope,
MemoryQuery {
agent: Some("agent".into()),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].author_tenant_id.as_deref(), Some("acme"));
assert_eq!(results[0].author_user_id.as_deref(), Some("u-42"));
}
}