use a3s_memory::{
FileMemoryStore, InMemoryStore, MemoryItem, MemoryStore, MemoryType, RelevanceConfig,
};
use std::sync::Arc;
use tempfile::TempDir;
async fn contract_store_retrieve(store: &dyn MemoryStore) {
let item = MemoryItem::new("integration test content")
.with_importance(0.7)
.with_tag("integration")
.with_type(MemoryType::Semantic);
let id = item.id.clone();
store.store(item).await.unwrap();
let retrieved = store.retrieve(&id).await.unwrap().unwrap();
assert_eq!(retrieved.content, "integration test content");
assert_eq!(retrieved.importance, 0.7);
assert_eq!(retrieved.tags, vec!["integration"]);
assert_eq!(retrieved.memory_type, MemoryType::Semantic);
}
async fn contract_retrieve_nonexistent(store: &dyn MemoryStore) {
assert!(store.retrieve("does-not-exist").await.unwrap().is_none());
}
async fn contract_search(store: &dyn MemoryStore) {
store
.store(MemoryItem::new("rust async programming"))
.await
.unwrap();
store
.store(MemoryItem::new("rust error handling"))
.await
.unwrap();
store
.store(MemoryItem::new("python scripting"))
.await
.unwrap();
let results = store.search("rust", 10).await.unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| r.content.contains("rust")));
}
async fn contract_search_case_insensitive(store: &dyn MemoryStore) {
store
.store(MemoryItem::new("Rust Programming Language"))
.await
.unwrap();
let results = store.search("rust", 10).await.unwrap();
assert_eq!(results.len(), 1);
}
async fn contract_search_limit(store: &dyn MemoryStore) {
for i in 0..10 {
store
.store(MemoryItem::new(format!("item {i}")))
.await
.unwrap();
}
let results = store.search("item", 3).await.unwrap();
assert_eq!(results.len(), 3);
}
async fn contract_search_by_tags(store: &dyn MemoryStore) {
store
.store(MemoryItem::new("one").with_tags(vec!["rust".into(), "async".into()]))
.await
.unwrap();
store
.store(MemoryItem::new("two").with_tags(vec!["python".into()]))
.await
.unwrap();
store
.store(MemoryItem::new("three").with_tags(vec!["rust".into()]))
.await
.unwrap();
let results = store
.search_by_tags(&["rust".to_string()], 10)
.await
.unwrap();
assert_eq!(results.len(), 2);
let results = store
.search_by_tags(&["python".to_string()], 10)
.await
.unwrap();
assert_eq!(results.len(), 1);
let results = store.search_by_tags(&["go".to_string()], 10).await.unwrap();
assert_eq!(results.len(), 0);
}
async fn contract_get_recent(store: &dyn MemoryStore) {
use chrono::Utc;
for i in 0..5 {
let mut item = MemoryItem::new(format!("item {i}"));
item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
store.store(item).await.unwrap();
}
let results = store.get_recent(3).await.unwrap();
assert_eq!(results.len(), 3);
assert!(results[0].timestamp >= results[1].timestamp);
assert!(results[1].timestamp >= results[2].timestamp);
}
async fn contract_get_important(store: &dyn MemoryStore) {
store
.store(MemoryItem::new("low").with_importance(0.1))
.await
.unwrap();
store
.store(MemoryItem::new("medium").with_importance(0.5))
.await
.unwrap();
store
.store(MemoryItem::new("high").with_importance(0.9))
.await
.unwrap();
let results = store.get_important(0.7, 10).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].content, "high");
let results = store.get_important(0.0, 2).await.unwrap();
assert_eq!(results.len(), 2);
assert!(results[0].importance >= results[1].importance);
}
async fn contract_delete(store: &dyn MemoryStore) {
let item = MemoryItem::new("to delete");
let id = item.id.clone();
store.store(item).await.unwrap();
assert_eq!(store.count().await.unwrap(), 1);
store.delete(&id).await.unwrap();
assert_eq!(store.count().await.unwrap(), 0);
assert!(store.retrieve(&id).await.unwrap().is_none());
}
async fn contract_delete_nonexistent(store: &dyn MemoryStore) {
store.delete("nonexistent").await.unwrap();
}
async fn contract_clear(store: &dyn MemoryStore) {
for i in 0..5 {
store
.store(MemoryItem::new(format!("item {i}")))
.await
.unwrap();
}
assert_eq!(store.count().await.unwrap(), 5);
store.clear().await.unwrap();
assert_eq!(store.count().await.unwrap(), 0);
}
async fn contract_count(store: &dyn MemoryStore) {
assert_eq!(store.count().await.unwrap(), 0);
store.store(MemoryItem::new("one")).await.unwrap();
assert_eq!(store.count().await.unwrap(), 1);
store.store(MemoryItem::new("two")).await.unwrap();
assert_eq!(store.count().await.unwrap(), 2);
}
async fn contract_relevance_ordering(store: &dyn MemoryStore) {
store
.store(MemoryItem::new("rust tip").with_importance(0.2))
.await
.unwrap();
store
.store(MemoryItem::new("rust trick").with_importance(0.9))
.await
.unwrap();
let results = store.search("rust", 10).await.unwrap();
assert_eq!(results.len(), 2);
assert!(results[0].importance >= results[1].importance);
}
async fn contract_search_specificity_beats_generic_importance(store: &dyn MemoryStore) {
store
.store(
MemoryItem::new("memory")
.with_importance(1.0)
.with_type(MemoryType::Semantic),
)
.await
.unwrap();
store
.store(
MemoryItem::new(
"Run focused memory extraction tests after changing the agent memory pipeline.",
)
.with_importance(0.2)
.with_type(MemoryType::Procedural),
)
.await
.unwrap();
let results = store
.search("focused memory extraction tests", 10)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert!(results[0].content.contains("memory extraction tests"));
}
async fn contract_store_deduplicates_durable_content(store: &dyn MemoryStore) {
store
.store(
MemoryItem::new("Run focused memory extraction tests after parser changes.")
.with_importance(0.35)
.with_tag("memory")
.with_metadata("source", "workflow"),
)
.await
.unwrap();
store
.store(
MemoryItem::new(" run focused MEMORY extraction tests after parser changes! ")
.with_importance(0.9)
.with_tag("tests")
.with_metadata("supersedes", "old-memory"),
)
.await
.unwrap();
assert_eq!(store.count().await.unwrap(), 1);
let results = store
.search("focused memory extraction parser changes", 10)
.await
.unwrap();
assert_eq!(results.len(), 1);
let item = &results[0];
assert_eq!(item.importance, 0.9);
assert!(item.tags.contains(&"memory".to_string()));
assert!(item.tags.contains(&"tests".to_string()));
assert_eq!(
item.metadata.get("supersedes").map(String::as_str),
Some("old-memory")
);
assert_eq!(
item.metadata.get("duplicate_count").map(String::as_str),
Some("1")
);
}
async fn contract_store_merges_near_duplicate_content(store: &dyn MemoryStore) {
store
.store(
MemoryItem::new("Run focused memory store tests after parser changes.")
.with_importance(0.35)
.with_tag("memory")
.with_type(MemoryType::Procedural),
)
.await
.unwrap();
store
.store(
MemoryItem::new("Run focused memory store regression tests after parser changes.")
.with_importance(0.9)
.with_tag("tests")
.with_type(MemoryType::Procedural),
)
.await
.unwrap();
assert_eq!(store.count().await.unwrap(), 1);
let results = store
.search("focused memory store regression tests parser", 10)
.await
.unwrap();
assert_eq!(results.len(), 1);
let item = &results[0];
assert!(item.content.contains("regression tests"));
assert_eq!(item.importance, 0.9);
assert!(item.tags.contains(&"memory".to_string()));
assert!(item.tags.contains(&"tests".to_string()));
assert_eq!(
item.metadata.get("duplicate_count").map(String::as_str),
Some("1")
);
}
async fn contract_store_keeps_conflicting_near_duplicate_content(store: &dyn MemoryStore) {
store
.store(
MemoryItem::new("Use file memory store for local sessions.")
.with_type(MemoryType::Semantic),
)
.await
.unwrap();
store
.store(
MemoryItem::new("Do not use file memory store for local sessions.")
.with_type(MemoryType::Semantic),
)
.await
.unwrap();
assert_eq!(store.count().await.unwrap(), 2);
let results = store
.search("file memory store local sessions", 10)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|item| item.content.starts_with("Use ")));
assert!(results
.iter()
.any(|item| item.content.starts_with("Do not use")));
}
async fn contract_prune_protects_curated_memories(store: &dyn MemoryStore) {
let mut pinned = MemoryItem::new("Pinned low-importance memory.")
.with_importance(0.1)
.with_tag("pinned");
pinned.timestamp = chrono::Utc::now() - chrono::Duration::days(120);
store.store(pinned).await.unwrap();
let mut accessed =
MemoryItem::new("Frequently recalled low-importance memory.").with_importance(0.1);
accessed.timestamp = chrono::Utc::now() - chrono::Duration::days(120);
accessed.record_access();
accessed.record_access();
accessed.record_access();
store.store(accessed).await.unwrap();
let mut related = MemoryItem::new("Conflict memory should remain auditable.")
.with_importance(0.1)
.with_metadata("conflicts_with", "legacy-memory");
related.timestamp = chrono::Utc::now() - chrono::Duration::days(120);
store.store(related).await.unwrap();
let mut stale = MemoryItem::new("Unprotected stale memory.").with_importance(0.1);
stale.timestamp = chrono::Utc::now() - chrono::Duration::days(120);
store.store(stale).await.unwrap();
let policy = a3s_memory::PrunePolicy {
max_age_days: 90,
min_importance_to_keep: 0.5,
max_items: 2,
};
let deleted = store.prune(&policy).await.unwrap();
assert_eq!(deleted, 1);
let results = store.search("memory", 10).await.unwrap();
assert_eq!(results.len(), 3);
assert!(results
.iter()
.any(|item| item.tags.contains(&"pinned".into())));
assert!(results.iter().any(|item| item.access_count >= 3));
assert!(results
.iter()
.any(|item| item.metadata.contains_key("conflicts_with")));
assert!(!results
.iter()
.any(|item| item.content.contains("Unprotected stale")));
}
macro_rules! in_memory_test {
($name:ident, $fn:ident) => {
#[tokio::test]
async fn $name() {
$fn(&InMemoryStore::new()).await;
}
};
}
in_memory_test!(in_memory_store_retrieve, contract_store_retrieve);
in_memory_test!(
in_memory_retrieve_nonexistent,
contract_retrieve_nonexistent
);
in_memory_test!(in_memory_search, contract_search);
in_memory_test!(
in_memory_search_case_insensitive,
contract_search_case_insensitive
);
in_memory_test!(in_memory_search_limit, contract_search_limit);
in_memory_test!(in_memory_search_by_tags, contract_search_by_tags);
in_memory_test!(in_memory_get_recent, contract_get_recent);
in_memory_test!(in_memory_get_important, contract_get_important);
in_memory_test!(in_memory_delete, contract_delete);
in_memory_test!(in_memory_delete_nonexistent, contract_delete_nonexistent);
in_memory_test!(in_memory_clear, contract_clear);
in_memory_test!(in_memory_count, contract_count);
in_memory_test!(in_memory_relevance_ordering, contract_relevance_ordering);
in_memory_test!(
in_memory_search_specificity_beats_generic_importance,
contract_search_specificity_beats_generic_importance
);
in_memory_test!(
in_memory_store_deduplicates_durable_content,
contract_store_deduplicates_durable_content
);
in_memory_test!(
in_memory_store_merges_near_duplicate_content,
contract_store_merges_near_duplicate_content
);
in_memory_test!(
in_memory_store_keeps_conflicting_near_duplicate_content,
contract_store_keeps_conflicting_near_duplicate_content
);
in_memory_test!(
in_memory_prune_protects_curated_memories,
contract_prune_protects_curated_memories
);
macro_rules! file_store_test {
($name:ident, $fn:ident) => {
#[tokio::test]
async fn $name() {
let dir = TempDir::new().unwrap();
let store = FileMemoryStore::new(dir.path()).await.unwrap();
$fn(&store).await;
}
};
}
file_store_test!(file_store_retrieve, contract_store_retrieve);
file_store_test!(file_retrieve_nonexistent, contract_retrieve_nonexistent);
file_store_test!(file_search, contract_search);
file_store_test!(
file_search_case_insensitive,
contract_search_case_insensitive
);
file_store_test!(file_search_limit, contract_search_limit);
file_store_test!(file_search_by_tags, contract_search_by_tags);
file_store_test!(file_get_recent, contract_get_recent);
file_store_test!(file_get_important, contract_get_important);
file_store_test!(file_delete, contract_delete);
file_store_test!(file_delete_nonexistent, contract_delete_nonexistent);
file_store_test!(file_clear, contract_clear);
file_store_test!(file_count, contract_count);
file_store_test!(file_relevance_ordering, contract_relevance_ordering);
file_store_test!(
file_search_specificity_beats_generic_importance,
contract_search_specificity_beats_generic_importance
);
file_store_test!(
file_store_deduplicates_durable_content,
contract_store_deduplicates_durable_content
);
file_store_test!(
file_store_merges_near_duplicate_content,
contract_store_merges_near_duplicate_content
);
file_store_test!(
file_store_keeps_conflicting_near_duplicate_content,
contract_store_keeps_conflicting_near_duplicate_content
);
file_store_test!(
file_prune_protects_curated_memories,
contract_prune_protects_curated_memories
);
#[tokio::test]
async fn file_store_persists_across_instances() {
let dir = TempDir::new().unwrap();
{
let store = FileMemoryStore::new(dir.path()).await.unwrap();
store
.store(MemoryItem::new("persistent").with_tags(vec!["test".into()]))
.await
.unwrap();
}
{
let store = FileMemoryStore::new(dir.path()).await.unwrap();
assert_eq!(store.count().await.unwrap(), 1);
let results = store.search("persistent", 10).await.unwrap();
assert_eq!(results[0].content, "persistent");
assert_eq!(results[0].tags, vec!["test"]);
}
}
#[tokio::test]
async fn file_store_rebuild_index_recovers_from_corruption() {
let dir = TempDir::new().unwrap();
{
let store = FileMemoryStore::new(dir.path()).await.unwrap();
store.store(MemoryItem::new("alpha")).await.unwrap();
store.store(MemoryItem::new("beta")).await.unwrap();
}
tokio::fs::remove_file(dir.path().join("index.json"))
.await
.unwrap();
{
let store = FileMemoryStore::new(dir.path()).await.unwrap();
assert_eq!(store.count().await.unwrap(), 0); let recovered = store.rebuild_index().await.unwrap();
assert_eq!(recovered, 2);
assert_eq!(store.count().await.unwrap(), 2);
}
}
#[tokio::test]
async fn file_store_path_traversal_prevention() {
let dir = TempDir::new().unwrap();
let store = FileMemoryStore::new(dir.path()).await.unwrap();
let mut item = MemoryItem::new("sneaky");
item.id = "../../../etc/passwd".to_string();
store.store(item).await.unwrap();
let results = store.search("sneaky", 10).await.unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].id.contains('/'));
assert!(!results[0].id.contains(".."));
}
#[test]
fn relevance_config_custom_weights() {
let item = MemoryItem::new("test").with_importance(1.0);
let now = chrono::Utc::now();
let aggressive = RelevanceConfig {
decay_days: 1.0,
importance_weight: 0.5,
recency_weight: 0.5,
};
let conservative = RelevanceConfig {
decay_days: 365.0,
importance_weight: 0.9,
recency_weight: 0.1,
};
let score_aggressive = item.relevance_score_at(now, &aggressive);
let score_conservative = item.relevance_score_at(now, &conservative);
assert!(score_aggressive > 0.9);
assert!(score_conservative > 0.9);
}
#[test]
fn relevance_score_old_item_decays() {
let mut item = MemoryItem::new("old").with_importance(0.5);
item.timestamp = chrono::Utc::now() - chrono::Duration::days(90);
let config = RelevanceConfig::default(); let score = item.relevance_score_at(chrono::Utc::now(), &config);
assert!(score < 0.40, "score was {score}");
}
#[test]
fn memory_item_builder_chain() {
let item = MemoryItem::new("content")
.with_importance(0.8)
.with_tags(vec!["a".into(), "b".into()])
.with_tag("c")
.with_type(MemoryType::Procedural)
.with_metadata("key", "value");
assert_eq!(item.content, "content");
assert_eq!(item.importance, 0.8);
assert_eq!(item.tags, vec!["a", "b", "c"]);
assert_eq!(item.memory_type, MemoryType::Procedural);
assert_eq!(item.metadata.get("key").unwrap(), "value");
}
#[test]
fn memory_item_with_tags_replaces() {
let item = MemoryItem::new("x")
.with_tag("a")
.with_tag("b")
.with_tags(vec!["c".into()]); assert_eq!(item.tags, vec!["c"]);
}
#[test]
fn memory_item_importance_clamped() {
assert_eq!(MemoryItem::new("x").with_importance(2.0).importance, 1.0);
assert_eq!(MemoryItem::new("x").with_importance(-1.0).importance, 0.0);
}
#[tokio::test]
async fn memory_store_is_object_safe() {
let stores: Vec<Arc<dyn MemoryStore>> = vec![Arc::new(InMemoryStore::new())];
for store in &stores {
store.store(MemoryItem::new("test")).await.unwrap();
assert_eq!(store.count().await.unwrap(), 1);
}
}