#![allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::needless_borrows_for_generic_args
)]
use entelix_core::TenantId;
use std::sync::Arc;
use entelix_core::ExecutionContext;
use entelix_core::Result;
use entelix_core::ir::Message;
use entelix_memory::{BufferMemory, Document, InMemoryStore, Namespace, Store};
#[test]
fn namespace_render_with_no_scope_is_just_tenant() {
let ns = Namespace::new(TenantId::new("tenant-A"));
assert_eq!(ns.tenant_id(), "tenant-A");
assert!(ns.scope().is_empty());
assert_eq!(ns.render(), "tenant-A");
}
#[test]
fn namespace_render_joins_scope_with_colons() {
let ns = Namespace::new(TenantId::new("tenant-A"))
.with_scope("agent-1")
.with_scope("conversation-42");
assert_eq!(ns.render(), "tenant-A:agent-1:conversation-42");
assert_eq!(
ns.scope(),
&["agent-1".to_owned(), "conversation-42".to_owned()]
);
}
#[test]
fn namespaces_are_distinct_when_tenant_differs() {
let a = Namespace::new(TenantId::new("alpha")).with_scope("x");
let b = Namespace::new(TenantId::new("beta")).with_scope("x");
assert_ne!(a.render(), b.render());
}
#[tokio::test]
async fn memory_store_put_get_delete_round_trip() -> Result<()> {
let store = InMemoryStore::<i64>::new();
let ns = Namespace::new(TenantId::new("t"));
let ctx = ExecutionContext::new();
store.put(&ctx, &ns, "k1", 42).await?;
assert_eq!(store.get(&ctx, &ns, "k1").await?, Some(42));
store.delete(&ctx, &ns, "k1").await?;
assert_eq!(store.get(&ctx, &ns, "k1").await?, None);
Ok(())
}
#[tokio::test]
async fn memory_store_namespaces_are_isolated() -> Result<()> {
let store = InMemoryStore::<i64>::new();
let alpha = Namespace::new(TenantId::new("alpha"));
let beta = Namespace::new(TenantId::new("beta"));
let ctx = ExecutionContext::new();
store.put(&ctx, &alpha, "k", 1).await?;
store.put(&ctx, &beta, "k", 2).await?;
assert_eq!(store.get(&ctx, &alpha, "k").await?, Some(1));
assert_eq!(store.get(&ctx, &beta, "k").await?, Some(2));
assert_eq!(store.total_entries(), 2);
Ok(())
}
#[tokio::test]
async fn memory_store_scope_segments_isolate_namespaces() -> Result<()> {
let store = InMemoryStore::<String>::new();
let agent_a = Namespace::new(TenantId::new("tenant")).with_scope("agent-A");
let agent_b = Namespace::new(TenantId::new("tenant")).with_scope("agent-B");
let ctx = ExecutionContext::new();
store.put(&ctx, &agent_a, "name", "Alice".into()).await?;
store.put(&ctx, &agent_b, "name", "Bob".into()).await?;
assert_eq!(
store.get(&ctx, &agent_a, "name").await?.as_deref(),
Some("Alice")
);
assert_eq!(
store.get(&ctx, &agent_b, "name").await?.as_deref(),
Some("Bob")
);
Ok(())
}
#[tokio::test]
async fn memory_store_list_with_prefix_filters_correctly() -> Result<()> {
let store = InMemoryStore::<i32>::new();
let ns = Namespace::new(TenantId::new("t"));
let ctx = ExecutionContext::new();
store.put(&ctx, &ns, "user.alice", 1).await?;
store.put(&ctx, &ns, "user.bob", 2).await?;
store.put(&ctx, &ns, "agent.helper", 3).await?;
let mut user_keys = store.list(&ctx, &ns, Some("user.")).await?;
user_keys.sort();
assert_eq!(
user_keys,
vec!["user.alice".to_owned(), "user.bob".to_owned()]
);
let all = store.list(&ctx, &ns, None).await?;
assert_eq!(all.len(), 3);
Ok(())
}
#[tokio::test]
async fn memory_store_delete_is_idempotent() -> Result<()> {
let store = InMemoryStore::<i32>::new();
let ns = Namespace::new(TenantId::new("t"));
let ctx = ExecutionContext::new();
store.delete(&ctx, &ns, "missing").await?; Ok(())
}
#[tokio::test]
async fn memory_store_dyn_dispatch_works_via_arc_store() -> Result<()> {
let store: Arc<dyn Store<String>> = Arc::new(InMemoryStore::<String>::new());
let ns = Namespace::new(TenantId::new("t"));
let ctx = ExecutionContext::new();
store.put(&ctx, &ns, "k", "v".into()).await?;
assert_eq!(store.get(&ctx, &ns, "k").await?.as_deref(), Some("v"));
Ok(())
}
#[tokio::test]
async fn buffer_memory_appends_and_returns_messages() -> Result<()> {
let store: Arc<dyn Store<Vec<Message>>> = Arc::new(InMemoryStore::<Vec<Message>>::new());
let buf = BufferMemory::new(store, Namespace::new(TenantId::new("tenant")), 10);
let ctx = ExecutionContext::new();
buf.append(&ctx, Message::user("hi")).await?;
buf.append(&ctx, Message::assistant("hello")).await?;
let messages = buf.messages(&ctx).await?;
assert_eq!(messages.len(), 2);
Ok(())
}
#[tokio::test]
async fn buffer_memory_drops_oldest_when_over_capacity() -> Result<()> {
let store: Arc<dyn Store<Vec<Message>>> = Arc::new(InMemoryStore::<Vec<Message>>::new());
let buf = BufferMemory::new(store, Namespace::new(TenantId::new("tenant")), 3);
let ctx = ExecutionContext::new();
for i in 0..5 {
buf.append(&ctx, Message::user(&format!("turn {i}")))
.await?;
}
let messages = buf.messages(&ctx).await?;
assert_eq!(messages.len(), 3);
let texts: Vec<String> = messages
.iter()
.filter_map(|m| match m.content.first() {
Some(entelix_core::ir::ContentPart::Text { text, .. }) => Some(text.clone()),
_ => None,
})
.collect();
assert_eq!(texts, vec!["turn 2", "turn 3", "turn 4"]);
Ok(())
}
#[tokio::test]
async fn buffer_memory_clear_resets_to_empty() -> Result<()> {
let store: Arc<dyn Store<Vec<Message>>> = Arc::new(InMemoryStore::<Vec<Message>>::new());
let buf = BufferMemory::new(store, Namespace::new(TenantId::new("tenant")), 10);
let ctx = ExecutionContext::new();
buf.append(&ctx, Message::user("hi")).await?;
assert_eq!(buf.messages(&ctx).await?.len(), 1);
buf.clear(&ctx).await?;
assert_eq!(buf.messages(&ctx).await?.len(), 0);
Ok(())
}
#[tokio::test]
async fn buffer_memory_namespaces_are_isolated() -> Result<()> {
let store: Arc<dyn Store<Vec<Message>>> = Arc::new(InMemoryStore::<Vec<Message>>::new());
let buf_a = BufferMemory::new(
store.clone(),
Namespace::new(TenantId::new("tenant")).with_scope("A"),
10,
);
let buf_b = BufferMemory::new(
store,
Namespace::new(TenantId::new("tenant")).with_scope("B"),
10,
);
let ctx = ExecutionContext::new();
buf_a.append(&ctx, Message::user("from A")).await?;
buf_b.append(&ctx, Message::user("from B")).await?;
assert_eq!(buf_a.messages(&ctx).await?.len(), 1);
assert_eq!(buf_b.messages(&ctx).await?.len(), 1);
Ok(())
}
#[test]
fn document_builders_set_fields() {
let d = Document::new("hello").with_metadata(serde_json::json!({"src": "test"}));
assert_eq!(d.content, "hello");
assert_eq!(d.metadata["src"], "test");
assert!(d.score.is_none());
}