use chrono::{DateTime, Utc};
use mindcore::context::ContextBudget;
use mindcore::engine::MemoryEngine;
use mindcore::scoring::{CompositeScorer, ImportanceScorer, RecencyScorer, MemoryTypeScorer};
use mindcore::traits::{MemoryRecord, MemoryType};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Mem {
id: Option<i64>,
text: String,
importance: u8,
mem_type: MemoryType,
created_at: DateTime<Utc>,
}
impl MemoryRecord for Mem {
fn id(&self) -> Option<i64> { self.id }
fn searchable_text(&self) -> String { self.text.clone() }
fn memory_type(&self) -> MemoryType { self.mem_type }
fn importance(&self) -> u8 { self.importance }
fn created_at(&self) -> DateTime<Utc> { self.created_at }
}
fn mem(text: &str, importance: u8, mem_type: MemoryType) -> Mem {
Mem {
id: None,
text: text.into(),
importance,
mem_type,
created_at: Utc::now(),
}
}
#[test]
fn scoring_affects_result_order() {
let engine = MemoryEngine::<Mem>::builder()
.scoring(ImportanceScorer::default())
.build()
.expect("build");
engine.store(&mem("search target low importance", 1, MemoryType::Semantic)).expect("store");
engine.store(&mem("search target high importance", 10, MemoryType::Semantic)).expect("store");
let results = engine.search("search target").limit(10).execute().expect("search");
assert_eq!(results.len(), 2);
let first = &results[0];
let second = &results[1];
assert!(
first.score >= second.score,
"high importance should score higher: first={}, second={}",
first.score, second.score
);
}
#[test]
fn composite_scorer_on_engine() {
let scorer = CompositeScorer::new(vec![
Box::new(RecencyScorer::default_half_life()),
Box::new(ImportanceScorer::default()),
Box::new(MemoryTypeScorer::default()),
]);
let engine = MemoryEngine::<Mem>::builder()
.scoring(scorer)
.build()
.expect("build");
engine.store(&mem("composite scoring test one", 8, MemoryType::Procedural)).expect("store");
engine.store(&mem("composite scoring test two", 3, MemoryType::Episodic)).expect("store");
let results = engine.search("composite scoring").limit(10).execute().expect("search");
assert_eq!(results.len(), 2);
assert!(results[0].score > results[1].score);
}
#[test]
fn context_assembly_via_engine() {
let engine = MemoryEngine::<Mem>::builder().build().expect("build");
engine.store(&mem("context assembly test alpha", 5, MemoryType::Semantic)).expect("store");
engine.store(&mem("context assembly test beta", 7, MemoryType::Semantic)).expect("store");
engine.store(&mem("unrelated memory about cats", 5, MemoryType::Episodic)).expect("store");
let budget = ContextBudget::new(1000);
let assembly = engine.assemble_context("context assembly", &budget).expect("assemble");
assert_eq!(assembly.items.len(), 2, "should find 2 matching memories");
assert!(!assembly.is_truncated());
let rendered = assembly.render();
assert!(rendered.contains("alpha"));
assert!(rendered.contains("beta"));
assert!(!rendered.contains("cats"));
}
#[test]
fn context_assembly_respects_budget() {
let engine = MemoryEngine::<Mem>::builder().build().expect("build");
for i in 0..20 {
engine
.store(&mem(
&format!("budget test memory item number {i} with some extra text to take up tokens"),
5,
MemoryType::Semantic,
))
.expect("store");
}
let budget = ContextBudget::new(50);
let assembly = engine.assemble_context("budget test", &budget).expect("assemble");
assert!(assembly.items.len() < 20, "budget should limit items");
assert!(assembly.total_tokens <= 50);
assert!(assembly.is_truncated());
}
#[test]
fn no_scorer_uses_raw_scores() {
let engine = MemoryEngine::<Mem>::builder().build().expect("build");
engine.store(&mem("raw score test item", 5, MemoryType::Semantic)).expect("store");
let results = engine.search("raw score").execute().expect("search");
assert_eq!(results.len(), 1);
assert!(results[0].score > 0.0);
}