use async_trait::async_trait;
use khive_brain_core::PackTunable;
use khive_pack_kg::KgPack;
use khive_pack_memory::MemoryPack;
use khive_runtime::{
EmbedderProvider, FusionStrategy, KhiveRuntime, Namespace, RuntimeConfig, VerbRegistryBuilder,
};
use khive_types::Pack;
use lattice_embed::{EmbedError, EmbeddingModel, EmbeddingService};
use serde_json::json;
use std::sync::Arc;
use uuid::Uuid;
fn make_runtime() -> KhiveRuntime {
KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
..RuntimeConfig::default()
})
.expect("in-memory runtime")
}
fn make_registry(rt: KhiveRuntime) -> khive_runtime::VerbRegistry {
let mut builder = VerbRegistryBuilder::new();
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt));
builder.build().expect("registry builds")
}
#[tokio::test]
async fn test_remember_dispatch_succeeds_with_no_embeddings_runtime() {
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
packs: vec!["kg".to_string()],
..RuntimeConfig::no_embeddings()
})
.expect("no_embeddings runtime");
assert!(
rt.registered_embedding_model_names().is_empty(),
"no_embeddings() runtime must register zero embedders"
);
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "issue-396 regression: memory.remember via real dispatch with zero embedders",
"memory_type": "semantic",
"salience": 0.7
}),
)
.await
.expect("memory.remember must succeed with zero registered embedders");
let note_id = result["id"].as_str().expect("has note_id");
assert!(!note_id.is_empty());
}
#[tokio::test]
async fn test_remember_recall_smoke() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "The attention mechanism in transformers uses Q K V matrices",
"memory_type": "semantic",
"salience": 0.8,
"decay": 0.01
}),
)
.await
.expect("memory.remember succeeds");
let note_id = result["id"].as_str().expect("has note_id");
assert!(!note_id.is_empty());
let recall_result = registry
.dispatch(
"memory.recall",
json!({ "query": "attention mechanism transformers" }),
)
.await
.expect("memory.recall succeeds");
let hits = recall_result.as_array().expect("array of hits");
assert!(!hits.is_empty(), "recall returned at least one result");
let first_id = hits[0]["id"].as_str().unwrap();
assert_eq!(first_id, note_id, "recalled the memory we just created");
}
#[tokio::test]
async fn test_recall_decay_ranking() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let shared_content = "memory about neural networks and deep learning";
let fresh = registry
.dispatch(
"memory.remember",
json!({
"content": shared_content,
"salience": 0.7,
"decay": 0.01
}),
)
.await
.expect("fresh remember");
let fresh_id = fresh["id"].as_str().unwrap().to_string();
let old = registry
.dispatch(
"memory.remember",
json!({
"content": shared_content,
"salience": 0.7,
"decay": 0.1
}),
)
.await
.expect("old remember");
let old_id = old["id"].as_str().unwrap().to_string();
let old_uuid: uuid::Uuid = old_id.parse().unwrap();
let note_store = rt
.notes(&rt.authorize(Namespace::local()).unwrap())
.unwrap();
let mut old_note = note_store.get_note(old_uuid).await.unwrap().unwrap();
old_note.created_at -= 90 * 86_400_000_000i64; note_store.upsert_note(old_note).await.unwrap();
let recall_result = registry
.dispatch(
"memory.recall",
json!({
"query": "neural networks deep learning",
"config": {
"scoring": {
"mmr_penalty": 0.0
}
}
}),
)
.await
.expect("recall succeeds");
let hits = recall_result.as_array().expect("array");
let ranks: Vec<(&str, f64)> = hits
.iter()
.map(|h| {
(
h["id"].as_str().unwrap(),
h["rank_score"].as_f64().unwrap_or(0.0),
)
})
.collect();
let fresh_entry = ranks
.iter()
.find(|(id, _)| *id == fresh_id)
.expect("fresh in results");
let old_entry = ranks
.iter()
.find(|(id, _)| *id == old_id)
.expect("old in results");
assert!(
fresh_entry.1 > old_entry.1,
"fresh memory (rank_score={}) should rank higher than 90-day-old high-decay memory (rank_score={})",
fresh_entry.1,
old_entry.1
);
}
#[tokio::test]
async fn test_recall_salience_ranking() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let high = registry
.dispatch(
"memory.remember",
json!({
"content": "high-salience concept about knowledge representation theory",
"salience": 0.9,
"decay": 0.0
}),
)
.await
.expect("high salience remember");
let high_id = high["id"].as_str().unwrap().to_string();
let low = registry
.dispatch(
"memory.remember",
json!({
"content": "low-salience concept about knowledge representation systems",
"salience": 0.1,
"decay": 0.0
}),
)
.await
.expect("low salience remember");
let low_id = low["id"].as_str().unwrap().to_string();
let recall_result = registry
.dispatch(
"memory.recall",
json!({ "query": "knowledge representation" }),
)
.await
.expect("recall succeeds");
let hits = recall_result.as_array().expect("array");
let ranks: Vec<(&str, f64)> = hits
.iter()
.map(|h| {
(
h["id"].as_str().unwrap(),
h["rank_score"].as_f64().unwrap_or(0.0),
)
})
.collect();
let high_entry = ranks
.iter()
.find(|(id, _)| *id == high_id)
.expect("high in results");
let low_entry = ranks
.iter()
.find(|(id, _)| *id == low_id)
.expect("low in results");
assert!(
high_entry.1 >= low_entry.1,
"high salience memory (rank_score={}) should rank >= low salience (rank_score={})",
high_entry.1,
low_entry.1
);
}
#[tokio::test]
async fn test_recall_memory_type_filter() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
json!({
"content": "episodic event about meeting with Alice",
"memory_type": "episodic",
"salience": 0.7
}),
)
.await
.expect("episodic remember");
let semantic = registry
.dispatch(
"memory.remember",
json!({
"content": "semantic fact about meeting protocols",
"memory_type": "semantic",
"salience": 0.7
}),
)
.await
.expect("semantic remember");
let semantic_id = semantic["id"].as_str().unwrap().to_string();
let filtered = registry
.dispatch(
"memory.recall",
json!({ "query": "meeting", "memory_type": "semantic" }),
)
.await
.expect("recall with filter");
let hits = filtered.as_array().expect("array");
assert!(!hits.is_empty(), "got results with memory_type filter");
for hit in hits {
let mt = hit["memory_type"].as_str().unwrap_or("");
assert_eq!(mt, "semantic", "only semantic results returned");
}
let ids: Vec<&str> = hits.iter().map(|h| h["id"].as_str().unwrap()).collect();
assert!(
ids.contains(&semantic_id.as_str()),
"semantic note is in results"
);
}
#[test]
fn test_memory_pack_requires_kg() {
assert_eq!(MemoryPack::REQUIRES, &["kg"]);
assert_eq!(MemoryPack::NAME, "memory");
assert_eq!(MemoryPack::NOTE_KINDS, &["memory"]);
}
#[tokio::test]
async fn test_remember_source_id_not_in_properties() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let source = registry
.dispatch(
"create",
json!({
"kind": "person",
"name": "Alice",
"description": "test source person"
}),
)
.await
.expect("create source entity");
let source_uuid = source["id"].as_str().unwrap().to_string();
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "memory with a source",
"source": source_uuid
}),
)
.await
.expect("remember with source_id");
let note_id: Uuid = result["id"].as_str().unwrap().parse().expect("valid uuid");
let note_store = rt
.notes(&rt.authorize(Namespace::local()).unwrap())
.expect("note store");
let note = note_store
.get_note(note_id)
.await
.expect("get note")
.expect("note exists");
if let Some(props) = ¬e.properties {
assert!(
props.get("source_id").is_none(),
"source_id must not be stored in note properties; got: {props:?}"
);
}
}
#[tokio::test]
async fn test_remember_decay_factor_no_upper_cap() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "memory with high decay rate",
"decay": 5.0
}),
)
.await
.expect("remember with decay_factor > 1.0 should succeed");
let note_id: Uuid = result["id"].as_str().unwrap().parse().expect("valid uuid");
let note_store = rt
.notes(&rt.authorize(Namespace::local()).unwrap())
.expect("note store");
let note = note_store
.get_note(note_id)
.await
.expect("get note")
.expect("note exists");
let df = note.decay_factor.unwrap_or(0.0);
assert!(
(df - 5.0).abs() < 1e-10,
"decay_factor should be stored as-is (5.0), got {df}"
);
}
#[tokio::test]
async fn test_remember_decay_factor_negative_rejected() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "memory with negative decay",
"decay": -0.1
}),
)
.await;
assert!(result.is_err(), "negative decay_factor must be rejected");
}
#[tokio::test]
async fn test_remember_default_memory_type_written_to_properties() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({ "content": "memory without explicit type" }),
)
.await
.expect("remember without memory_type");
let note_id: Uuid = result["id"].as_str().unwrap().parse().expect("valid uuid");
assert_eq!(
result["memory_type"].as_str(),
Some("episodic"),
"response must include default memory_type"
);
let note_store = rt
.notes(&rt.authorize(Namespace::local()).unwrap())
.expect("note store");
let note = note_store
.get_note(note_id)
.await
.expect("get note")
.expect("note exists");
let stored_type = note
.properties
.as_ref()
.and_then(|p| p.get("memory_type"))
.and_then(|v| v.as_str());
assert_eq!(
stored_type,
Some("episodic"),
"memory_type must be written to properties even when not supplied"
);
}
#[tokio::test]
async fn test_remember_invalid_source_id_uuid_rejected() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "memory with bad source_id",
"source": "not-a-valid-uuid"
}),
)
.await;
assert!(
result.is_err(),
"invalid source_id UUID must cause an error, got: {result:?}"
);
}
#[tokio::test]
async fn test_remember_salience_out_of_range_rejected() {
let rt = make_runtime();
let registry = make_registry(rt);
let neg = registry
.dispatch(
"memory.remember",
json!({ "content": "test", "salience": -0.1 }),
)
.await;
assert!(neg.is_err(), "negative salience must be rejected");
let rt2 = make_runtime();
let registry2 = make_registry(rt2);
let above = registry2
.dispatch(
"memory.remember",
json!({ "content": "test", "salience": 1.1 }),
)
.await;
assert!(above.is_err(), "salience > 1 must be rejected");
}
#[tokio::test]
async fn test_recall_rerank_passthrough_with_no_active_rerankers() {
let rt = make_runtime();
let registry = make_registry(rt);
let candidates = json!([
{ "id": "00000000-0000-0000-0000-000000000001", "fused_score": 0.8 },
{ "id": "00000000-0000-0000-0000-000000000002", "fused_score": 0.6 },
]);
let result = registry
.dispatch("memory.recall_rerank", json!({ "candidates": candidates }))
.await
.expect("recall.rerank with no active rerankers");
let reranked = result["reranked"].as_array().expect("reranked array");
assert_eq!(reranked.len(), 2, "must return one entry per candidate");
for entry in reranked {
let scores = entry["rerank_scores"]
.as_object()
.expect("rerank_scores object");
assert!(
scores.is_empty(),
"no active rerankers → empty rerank_scores, got {scores:?}"
);
}
let active = result["active_rerankers"]
.as_array()
.expect("active_rerankers array");
assert!(active.is_empty(), "no active rerankers expected");
}
#[test]
fn test_memory_dotted_verbs_registered() {
let names: Vec<&str> = MemoryPack::HANDLERS.iter().map(|v| v.name).collect();
assert!(names.contains(&"memory.recall_candidates"));
assert!(names.contains(&"memory.recall_fuse"));
assert!(names.contains(&"memory.recall_score"));
assert!(names.contains(&"memory.recall_embed"));
assert!(
names.contains(&"memory.recall_rerank"),
"recall.rerank not found in: {names:?}"
);
}
#[tokio::test]
async fn test_recall_candidates_returns_arrays() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "attention recall candidates" }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch(
"memory.recall_candidates",
json!({ "query": "attention candidates" }),
)
.await
.expect("memory.recall_candidates");
let text = result["text_candidates"].as_array().expect("text array");
assert!(!text.is_empty());
assert!(text[0]["id"].as_str().is_some());
assert!(text[0]["score"].as_f64().is_some());
assert!(text[0]["rank"].as_u64().is_some());
assert!(result["candidate_limit"].as_u64().is_some());
assert!(
result.get("text_hits").is_none(),
"old count field must be absent"
);
}
#[tokio::test]
async fn test_recall_fuse_returns_fused_candidates_not_full_recall() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "attention fusion diagnostic" }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch("memory.recall_fuse", json!({ "query": "attention fusion" }))
.await
.expect("memory.recall_fuse");
let fused = result["fused_candidates"].as_array().expect("fused array");
assert!(!fused.is_empty());
assert!(fused[0]["fused_score"].as_f64().is_some());
assert!(fused[0]["source"].as_str().is_some());
assert!(
fused[0].get("content").is_none(),
"full recall field must be absent"
);
assert!(
fused[0].get("salience").is_none(),
"full recall field must be absent"
);
}
#[tokio::test]
async fn test_recall_breakdown_is_opt_in() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "attention score breakdown", "salience": 0.8 }),
)
.await
.expect("memory.remember");
let plain = registry
.dispatch("memory.recall", json!({ "query": "attention breakdown" }))
.await
.expect("memory.recall");
let hits = plain.as_array().unwrap();
assert!(!hits.is_empty());
assert!(
hits[0].get("breakdown").is_none(),
"breakdown must be absent by default"
);
let explained = registry
.dispatch(
"memory.recall",
json!({ "query": "attention breakdown", "config": { "include_breakdown": true } }),
)
.await
.expect("recall with breakdown");
let hits = explained.as_array().unwrap();
assert!(!hits.is_empty());
let bd = &hits[0]["breakdown"];
assert!(bd["relevance"].as_f64().is_some());
assert!(bd["salience_raw"].as_f64().is_some());
assert!(bd["salience_decayed"].as_f64().is_some());
assert!(bd["temporal"].as_f64().is_some());
assert!(bd["weighted"]["relevance_contribution"].as_f64().is_some());
}
#[tokio::test]
async fn test_recall_candidates_vector_field_always_present() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "text only candidate check" }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch(
"memory.recall_candidates",
json!({ "query": "text only candidate" }),
)
.await
.expect("memory.recall_candidates");
assert!(
result["vector_candidates"].as_array().is_some(),
"vector_candidates key must always be present"
);
assert!(
result["text_candidates"].as_array().is_some(),
"text_candidates key must always be present"
);
}
#[tokio::test]
async fn test_recall_fuse_source_field_is_plain_string() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "fuse source string check" }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch(
"memory.recall_fuse",
json!({ "query": "fuse source string" }),
)
.await
.expect("memory.recall_fuse");
let fused = result["fused_candidates"].as_array().expect("fused array");
assert!(!fused.is_empty());
let source = fused[0]["source"].as_str().expect("source is string");
assert!(
source == "text" || source == "vector" || source == "both",
"source must be a plain label, got {source:?}"
);
}
#[tokio::test]
async fn test_recall_fuse_rrf_k1_uses_retrieval_adapter() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "retrieval adapter rrf k1 probe memory" }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch(
"memory.recall_fuse",
json!({
"query": "retrieval adapter rrf k1 probe",
"config": {
"fuse_strategy": { "rrf": { "k": 1 } }
}
}),
)
.await
.expect("recall.fuse with Rrf k=1");
let fused = result["fused_candidates"].as_array().expect("fused array");
assert!(
!fused.is_empty(),
"recall.fuse must return at least one candidate"
);
let score = fused[0]["fused_score"]
.as_f64()
.expect("fused_score is f64");
let expected = 0.5_f64;
assert!(
(score - expected).abs() < 1e-6,
"RRF k=1, rank 1 → fused_score must be 0.5; got {score:.6} \
(≈0.0164 means the adapter passed k=60 instead of k=1)"
);
}
#[tokio::test]
async fn test_recall_fuse_shape_preserved_after_retrieval_wiring() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "shape regression check after retrieval wiring" }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch(
"memory.recall_fuse",
json!({ "query": "shape regression retrieval wiring" }),
)
.await
.expect("memory.recall_fuse");
assert!(
result.get("strategy").is_some(),
"strategy field must be present in recall.fuse response"
);
assert!(
result["candidate_limit"].as_u64().is_some(),
"candidate_limit must be a non-negative integer"
);
let fused = result["fused_candidates"]
.as_array()
.expect("fused_candidates array");
assert!(!fused.is_empty(), "fused_candidates must be non-empty");
let c = &fused[0];
assert!(c["id"].as_str().is_some(), "note_id must be a string UUID");
assert!(
c["fused_score"].as_f64().is_some(),
"fused_score must be a float"
);
let source = c["source"].as_str().expect("source must be a plain string");
assert!(
matches!(source, "text" | "vector" | "both"),
"source must be a plain label, got {source:?}"
);
assert!(
c.get("content").is_none(),
"content must be absent from recall.fuse output"
);
assert!(
c.get("salience").is_none(),
"salience must be absent from recall.fuse output"
);
}
#[tokio::test]
async fn test_recall_breakdown_total_matches_composite_score() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "arithmetic score check memory", "salience": 0.7 }),
)
.await
.expect("memory.remember");
let result = registry
.dispatch(
"memory.recall",
json!({ "query": "arithmetic score check", "config": { "include_breakdown": true } }),
)
.await
.expect("recall with breakdown");
let hits = result.as_array().unwrap();
assert!(!hits.is_empty());
let hit = &hits[0];
let rank_score = hit["rank_score"].as_f64().expect("hit has rank_score");
assert!(
(0.0..=1.0).contains(&rank_score),
"rank_score {rank_score} must be in [0, 1]"
);
let bd = &hit["breakdown"];
let rc = bd["weighted"]["relevance_contribution"].as_f64().unwrap();
let ic = bd["weighted"]["salience_contribution"].as_f64().unwrap();
let tc = bd["weighted"]["temporal_contribution"].as_f64().unwrap();
let total = rc + ic + tc;
assert!(
(0.0..=1.0).contains(&total),
"breakdown weighted sum {total} must be in [0, 1]"
);
}
#[tokio::test]
async fn test_recall_excludes_non_memory_notes() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let tok = rt.authorize(Namespace::local()).unwrap();
for i in 0..50 {
rt.create_note(
&tok,
"observation",
None,
&format!("observation {i} about attention mechanisms in neural networks"),
Some(0.5),
None,
vec![],
)
.await
.expect("create observation");
}
let mem1 = registry
.dispatch(
"memory.remember",
json!({
"content": "memory note about attention mechanisms in neural networks",
"salience": 0.8
}),
)
.await
.expect("remember 1");
let mem2 = registry
.dispatch(
"memory.remember",
json!({
"content": "another memory note about attention mechanisms",
"salience": 0.7
}),
)
.await
.expect("remember 2");
let mem1_id = mem1["id"].as_str().unwrap().to_string();
let mem2_id = mem2["id"].as_str().unwrap().to_string();
let result = registry
.dispatch(
"memory.recall",
json!({ "query": "attention mechanisms neural networks", "limit": 5 }),
)
.await
.expect("recall succeeds");
let hits = result.as_array().expect("array of hits");
assert!(
!hits.is_empty(),
"recall should return memory notes even when non-memory notes dominate the index"
);
let ids: Vec<&str> = hits.iter().map(|h| h["id"].as_str().unwrap()).collect();
assert!(
ids.contains(&mem1_id.as_str()) || ids.contains(&mem2_id.as_str()),
"at least one memory note must appear in recall results"
);
for hit in hits {
assert!(
hit.get("id").is_some(),
"hit has note_id field (memory pack shape)"
);
assert!(
hit.get("salience").is_some(),
"hit has salience field (memory pack shape)"
);
}
}
#[tokio::test]
async fn recall_candidate_cap_counts_eligible_memories_not_generic_notes() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let tok = rt.authorize(Namespace::local()).unwrap();
for i in 0..30 {
rt.create_note(
&tok,
"observation",
None,
&format!("observation {i} about quokka migration patterns in tundra"),
Some(0.9),
None,
vec![],
)
.await
.expect("create observation");
}
let want = 5usize;
let mut memory_ids = Vec::with_capacity(want);
for i in 0..want {
let mem = registry
.dispatch(
"memory.remember",
json!({
"content": format!("memory note {i} about quokka migration patterns in tundra"),
"salience": 0.6
}),
)
.await
.expect("remember");
memory_ids.push(mem["id"].as_str().unwrap().to_string());
}
let result = registry
.dispatch(
"memory.recall",
json!({
"query": "quokka migration patterns tundra",
"limit": want,
"config": { "candidate_limit": 5 },
}),
)
.await
.expect("recall succeeds");
let hits = result.as_array().expect("array of hits");
assert_eq!(
hits.len(),
want,
"recall must return the requested count of memories even when non-memory \
notes dominate a small candidate window; got {} of {want}: {hits:?}",
hits.len()
);
let ids: std::collections::HashSet<&str> =
hits.iter().map(|h| h["id"].as_str().unwrap()).collect();
for mid in &memory_ids {
assert!(
ids.contains(mid.as_str()),
"expected memory {mid} in recall results, got {ids:?}"
);
}
}
#[tokio::test]
async fn test_pack_tunable_apply_config_affects_recall_score() {
use khive_pack_memory::config::RecallConfig;
let rt = make_runtime();
let pack = MemoryPack::new(rt.clone());
let salience_only = RecallConfig {
relevance_weight: 0.0,
salience_weight: 1.0,
temporal_weight: 0.0,
..RecallConfig::default()
};
pack.apply_config(serde_json::to_value(&salience_only).unwrap())
.expect("apply_config (salience-only) succeeds");
let mut builder = VerbRegistryBuilder::new();
builder.register(KgPack::new(rt.clone()));
builder.register(pack);
let registry = builder.build().expect("registry builds");
let result = registry
.dispatch(
"memory.recall_score",
json!({
"rrf": 1.0,
"salience": 0.0,
"decay_factor": 0.0,
"age_days": 0.0,
}),
)
.await
.expect("recall.score succeeds");
let total = result["total"].as_f64().expect("total is a number");
assert!(
total.abs() < 1e-9,
"under salience_weight=1.0, salience=0 → score=0; got {total}. \
If non-zero, MemoryPack::active_config() is not being used by \
recall.score (#159 regression)."
);
let rt2 = make_runtime();
let pack2 = MemoryPack::new(rt2.clone());
let relevance_only = RecallConfig {
relevance_weight: 1.0,
salience_weight: 0.0,
temporal_weight: 0.0,
fuse_strategy: FusionStrategy::Weighted {
weights: vec![0.5, 0.5],
},
..RecallConfig::default()
};
pack2
.apply_config(serde_json::to_value(&relevance_only).unwrap())
.expect("apply_config (relevance-only) succeeds");
let mut builder2 = VerbRegistryBuilder::new();
builder2.register(KgPack::new(rt2.clone()));
builder2.register(pack2);
let registry2 = builder2.build().expect("registry2 builds");
let result2 = registry2
.dispatch(
"memory.recall_score",
json!({
"rrf": 1.0,
"salience": 0.0,
"decay_factor": 0.0,
"age_days": 0.0,
}),
)
.await
.expect("recall.score (relevance-only) succeeds");
let total2 = result2["total"].as_f64().expect("total is a number");
assert!(
(total2 - 1.0).abs() < 1e-9,
"under relevance_weight=1.0 with rrf=1.0 (Weighted strategy) → score=1.0; got {total2}"
);
}
#[tokio::test]
async fn test_recall_default_identity() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
for content in [
"the mitochondria is the powerhouse of the cell",
"ribosomes synthesize proteins in the cell",
"the nucleus contains the cell's DNA",
"lysosomes digest cellular waste in the cell",
] {
registry
.dispatch(
"memory.remember",
json!({ "content": content, "salience": 0.8 }),
)
.await
.expect("remember succeeds");
}
let base = registry
.dispatch("memory.recall", json!({ "query": "cell" }))
.await
.expect("baseline recall succeeds");
let base_hits = base.as_array().expect("array");
assert!(
base_hits.len() >= 2,
"baseline must return at least two hits to make ordering meaningful, got {}",
base_hits.len()
);
let knobless = registry
.dispatch(
"memory.recall",
json!({
"query": "cell",
"top_k": null,
"fusion_strategy": null,
"score_floor": null,
}),
)
.await
.expect("recall with all knobs null succeeds");
let knobless_hits = knobless.as_array().expect("array");
assert_eq!(
base_hits.len(),
knobless_hits.len(),
"null knobs must not change result count"
);
for (i, (b, k)) in base_hits.iter().zip(knobless_hits.iter()).enumerate() {
assert_eq!(
b["id"].as_str(),
k["id"].as_str(),
"null knobs altered note_id at position {i}"
);
let bs = b["score"].as_f64().unwrap_or(0.0);
let ks = k["score"].as_f64().unwrap_or(0.0);
assert!(
(bs - ks).abs() < 1e-9,
"null knobs altered score at position {i}: baseline={bs} knobless={ks}"
);
}
}
#[tokio::test]
async fn test_recall_top_k_override() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
for i in 0..5 {
registry
.dispatch(
"memory.remember",
json!({
"content": format!("rust ownership memory safety concept {i}"),
"salience": 0.7
}),
)
.await
.expect("remember succeeds");
}
let result = registry
.dispatch(
"memory.recall",
json!({ "query": "rust ownership memory safety", "top_k": 2 }),
)
.await
.expect("recall with top_k=2 succeeds");
let hits = result.as_array().expect("array");
assert!(
hits.len() <= 2,
"top_k=2 must return at most 2 results, got {}",
hits.len()
);
let result1 = registry
.dispatch(
"memory.recall",
json!({ "query": "rust ownership memory safety", "top_k": 1 }),
)
.await
.expect("recall with top_k=1 succeeds");
let hits1 = result1.as_array().expect("array");
assert!(
hits1.len() <= 1,
"top_k=1 must return at most 1 result, got {}",
hits1.len()
);
}
#[tokio::test]
async fn test_recall_fusion_strategy_override() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
json!({
"content": "gradient descent optimization machine learning",
"salience": 0.8
}),
)
.await
.expect("remember succeeds");
for strategy in &["rrf", "weighted", "union", "vector_only", "keyword_only"] {
let result = registry
.dispatch(
"memory.recall",
json!({
"query": "gradient descent optimization",
"fusion_strategy": strategy
}),
)
.await
.unwrap_or_else(|e| panic!("recall with fusion_strategy={strategy:?} failed: {e}"));
assert!(
result.is_array(),
"fusion_strategy={strategy:?} must return an array, got {result}"
);
}
let err = registry
.dispatch(
"memory.recall",
json!({
"query": "gradient descent optimization",
"fusion_strategy": "bogus"
}),
)
.await;
assert!(err.is_err(), "invalid fusion_strategy must return an error");
let msg = err.unwrap_err().to_string();
assert!(
msg.contains("rrf")
&& msg.contains("weighted")
&& msg.contains("union")
&& msg.contains("vector_only")
&& msg.contains("keyword_only"),
"error message must list valid strategies, got: {msg}"
);
}
#[tokio::test]
async fn test_recall_score_floor() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
json!({
"content": "backpropagation neural network training algorithm",
"salience": 0.6
}),
)
.await
.expect("remember succeeds");
let base = registry
.dispatch(
"memory.recall",
json!({ "query": "backpropagation neural network" }),
)
.await
.expect("baseline recall succeeds");
let base_count = base.as_array().expect("array").len();
let floored = registry
.dispatch(
"memory.recall",
json!({
"query": "backpropagation neural network",
"score_floor": 0.99
}),
)
.await
.expect("recall with score_floor=0.99 succeeds");
let floored_hits = floored.as_array().expect("array");
assert!(
floored_hits.len() <= base_count,
"score_floor=0.99 must return ≤ baseline count ({base_count}), got {}",
floored_hits.len()
);
for hit in floored_hits {
let score = hit["score"].as_f64().expect("score is a number");
assert!(
score >= 0.99,
"score_floor=0.99: all returned scores must be ≥ 0.99, got {score}"
);
}
let zero_floor = registry
.dispatch(
"memory.recall",
json!({
"query": "backpropagation neural network",
"score_floor": 0.0
}),
)
.await
.expect("recall with score_floor=0.0 succeeds");
let zero_count = zero_floor.as_array().expect("array").len();
assert_eq!(
zero_count, base_count,
"score_floor=0.0 must return same count as no floor"
);
}
#[tokio::test]
async fn test_recall_with_empty_reranker_weights_is_passthrough() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
for i in 0..4 {
registry
.dispatch(
"memory.remember",
json!({
"content": format!("memory about deep learning topic {i}"),
"salience": 0.5 + (i as f64) * 0.1,
"decay": 0.0
}),
)
.await
.expect("memory.remember");
}
let baseline = registry
.dispatch("memory.recall", json!({ "query": "deep learning" }))
.await
.expect("baseline recall");
let baseline_ids: Vec<String> = baseline
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap().to_string())
.collect();
let with_empty_reranker = registry
.dispatch(
"memory.recall",
json!({
"query": "deep learning",
"config": { "reranker_weights": {} }
}),
)
.await
.expect("recall with empty reranker_weights");
let reranker_ids: Vec<String> = with_empty_reranker
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap().to_string())
.collect();
assert_eq!(
baseline_ids, reranker_ids,
"empty reranker_weights must be a pass-through — result ordering must match baseline"
);
}
#[tokio::test]
async fn test_recall_with_reranker_weights_changes_ordering() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
for _ in 0..3 {
registry
.dispatch(
"memory.remember",
json!({
"content": "gradient descent gradient descent gradient descent optimization",
"salience": 0.1,
"decay": 0.0
}),
)
.await
.expect("low salience remember");
}
let high_salience = registry
.dispatch(
"memory.remember",
json!({
"content": "gradient descent is a key technique in machine learning",
"salience": 0.95,
"decay": 0.0
}),
)
.await
.expect("high salience remember");
let high_id = high_salience["id"].as_str().unwrap().to_string();
let baseline = registry
.dispatch(
"memory.recall",
json!({
"query": "gradient descent",
"config": {
"relevance_weight": 1.0,
"salience_weight": 0.0,
"temporal_weight": 0.0
}
}),
)
.await
.expect("baseline recall");
let baseline_hits = baseline.as_array().expect("baseline array");
assert!(
baseline_hits.len() >= 2,
"need at least 2 results to test ordering change, got {}",
baseline_hits.len()
);
let baseline_ids: Vec<String> = baseline_hits
.iter()
.map(|h| h["id"].as_str().unwrap().to_string())
.collect();
let baseline_top = &baseline_ids[0];
assert_ne!(
baseline_top, &high_id,
"fixture error: high-salience note already ranks first in baseline; \
reranker change cannot be demonstrated. baseline={baseline_ids:?}"
);
let reranked = registry
.dispatch(
"memory.recall",
json!({
"query": "gradient descent",
"config": {
"reranker_weights": { "salience": 1.0 }
}
}),
)
.await
.expect("recall with salience reranker");
let reranked_hits = reranked.as_array().expect("reranked array");
assert!(!reranked_hits.is_empty(), "must get results");
let reranked_ids: Vec<String> = reranked_hits
.iter()
.map(|h| h["id"].as_str().unwrap().to_string())
.collect();
let top_id = &reranked_ids[0];
assert_eq!(
top_id, &high_id,
"salience=1.0 reranker must rank the highest-salience memory first; got {top_id} not {high_id}"
);
assert_ne!(
baseline_ids, reranked_ids,
"reranker must change the result ordering; baseline={baseline_ids:?} reranked={reranked_ids:?}"
);
}
#[tokio::test]
async fn test_rerank_subhandler_uses_request_weights() {
let rt = make_runtime();
let registry = make_registry(rt);
let candidates = json!([
{
"id": "00000000-0000-0000-0000-000000000001",
"fused_score": 0.9,
"source": "both"
},
{
"id": "00000000-0000-0000-0000-000000000002",
"fused_score": 0.3,
"source": "text"
}
]);
let result = registry
.dispatch(
"memory.recall_rerank",
json!({
"candidates": candidates,
"config": {
"reranker_weights": { "relevance": 1.0 }
}
}),
)
.await
.expect("recall.rerank succeeds");
let reranked = result["reranked"].as_array().expect("reranked array");
assert_eq!(reranked.len(), 2, "both candidates returned");
let score_for = |id: &str| -> f64 {
reranked
.iter()
.find(|c| c["id"].as_str() == Some(id))
.and_then(|c| c["rerank_score"].as_f64())
.unwrap_or(f64::NAN)
};
let score_high = score_for("00000000-0000-0000-0000-000000000001");
let score_low = score_for("00000000-0000-0000-0000-000000000002");
assert!(
score_high.is_finite() && score_low.is_finite(),
"rerank_score must be a finite number; got high={score_high} low={score_low}"
);
assert!(
score_high > score_low,
"candidate with fused_score=0.9 must outscore fused_score=0.3 under relevance reranker; \
got {score_high} vs {score_low}"
);
let active = result["active_rerankers"]
.as_array()
.expect("active_rerankers");
assert!(
active.iter().any(|v| v.as_str() == Some("relevance")),
"active_rerankers must include 'relevance'"
);
}
#[tokio::test]
async fn test_remember_source_id_accepts_short_id() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let entity = registry
.dispatch(
"create",
json!({
"kind": "concept",
"name": "attention mechanism",
"description": "QKV self-attention"
}),
)
.await
.expect("create entity");
let full_id = entity["id"].as_str().expect("entity has id");
let short_id: String = full_id.chars().filter(|c| c != &'-').take(8).collect();
assert_eq!(short_id.len(), 8, "derived short_id must be 8 chars");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "attention uses Q K V matrices",
"source_id": short_id,
}),
)
.await
.expect("remember with 8-char short source_id must succeed");
let note_id_str = result["id"].as_str().expect("has note_id");
let neighbors = registry
.dispatch(
"neighbors",
json!({
"id": note_id_str,
"direction": "out",
}),
)
.await
.expect("neighbors call succeeds");
let hits = neighbors.as_array().expect("neighbors returns array");
let found = hits.iter().any(|h| h["id"].as_str() == Some(full_id));
assert!(
found,
"annotates edge to entity {full_id} must appear in note neighbors; got: {hits:?}\n\
(short_id used: {short_id}, note_id: {note_id_str})"
);
}
#[test]
fn test_handler_def_recall_params_complete() {
use khive_types::Pack;
let recall_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.recall")
.expect("recall handler must be registered");
let param_names: Vec<&str> = recall_def.params.iter().map(|p| p.name).collect();
assert!(
param_names.contains(&"top_k"),
"recall HandlerDef must expose top_k param; got: {param_names:?}"
);
assert!(
param_names.contains(&"score_floor"),
"recall HandlerDef must expose score_floor param; got: {param_names:?}"
);
assert!(
param_names.contains(&"fusion_strategy"),
"recall HandlerDef must expose fusion_strategy param; got: {param_names:?}"
);
assert!(
param_names.contains(&"embedding_model"),
"recall HandlerDef must expose embedding_model param; got: {param_names:?}"
);
assert!(
param_names.contains(&"include_breakdown"),
"recall HandlerDef must expose include_breakdown param (not presentation); got: {param_names:?}"
);
assert!(
!param_names.contains(&"presentation"),
"recall HandlerDef must not expose verb-level presentation (ambiguous with MCP envelope); got: {param_names:?}"
);
}
#[test]
fn test_handler_def_remember_params_complete() {
use khive_types::Pack;
let remember_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.remember")
.expect("remember handler must be registered");
let param_names: Vec<&str> = remember_def.params.iter().map(|p| p.name).collect();
assert!(
param_names.contains(&"embedding_model"),
"remember HandlerDef must expose embedding_model param; got: {param_names:?}"
);
let decay_def = remember_def
.params
.iter()
.find(|p| p.name == "decay_factor")
.expect("decay_factor param must exist");
assert!(
decay_def.description.contains("0.02"),
"decay_factor description must document episodic default 0.02, got: {:?}",
decay_def.description
);
assert!(
decay_def.description.contains("0.005"),
"decay_factor description must document semantic default 0.005, got: {:?}",
decay_def.description
);
assert!(
!decay_def
.description
.starts_with("Decay rate 0.0–1.0 (default 0.1)"),
"decay_factor description must NOT say 'default 0.1', got: {:?}",
decay_def.description
);
}
#[tokio::test]
async fn test_score_floor_portable_across_fusion_strategies() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
for (i, content) in [
"transformer architecture uses attention mechanism",
"attention is all you need for transformer models",
"feedforward layers in transformer with self-attention",
"layer normalization helps transformer attention training",
"residual connections in transformer improve attention flow",
"positional encoding enables transformer attention over sequences",
"multi-head attention splits queries in transformer blocks",
"softmax function normalizes transformer attention scores",
"token embeddings feed transformer attention layers",
"output projection combines transformer multi-head attention",
]
.iter()
.enumerate()
{
let salience = 0.4 + 0.06 * (i as f64); registry
.dispatch(
"memory.remember",
json!({
"content": content,
"salience": salience,
"decay_factor": 0.0,
}),
)
.await
.expect("memory.remember");
}
let rrf_result = registry
.dispatch(
"memory.recall",
json!({
"query": "attention transformer",
"score_floor": 0.3_f64,
"fusion_strategy": "rrf",
"limit": 20,
}),
)
.await
.expect("recall rrf");
let weighted_result = registry
.dispatch(
"memory.recall",
json!({
"query": "attention transformer",
"score_floor": 0.3_f64,
"fusion_strategy": "weighted",
"limit": 20,
}),
)
.await
.expect("recall weighted");
let rrf_hits = rrf_result.as_array().expect("rrf array").len();
let weighted_hits = weighted_result.as_array().expect("weighted array").len();
assert!(
rrf_hits > 0,
"score_floor=0.3 with RRF strategy must return > 0 hits (got 0); \
RRF scores are not being normalized to [0,1]"
);
assert!(
weighted_hits > 0,
"score_floor=0.3 with Weighted strategy must return > 0 hits (got 0)"
);
}
#[tokio::test]
async fn test_recall_include_breakdown_flag_includes_breakdown() {
let rt = make_runtime();
let registry = make_registry(rt);
registry
.dispatch(
"memory.remember",
json!({ "content": "transformer positional encoding", "salience": 0.8 }),
)
.await
.expect("memory.remember");
let default_result = registry
.dispatch("memory.recall", json!({ "query": "transformer" }))
.await
.expect("recall default");
let default_hits = default_result.as_array().expect("array");
assert!(!default_hits.is_empty(), "must have hits");
assert!(
default_hits[0].get("breakdown").is_none(),
"default recall must NOT include breakdown"
);
let verbose_result = registry
.dispatch(
"memory.recall",
json!({ "query": "transformer", "include_breakdown": true }),
)
.await
.expect("recall with include_breakdown=true");
let verbose_hits = verbose_result.as_array().expect("array");
assert!(
!verbose_hits.is_empty(),
"include_breakdown=true must have hits"
);
let bd = verbose_hits[0]
.get("breakdown")
.expect("include_breakdown=true result must include breakdown");
assert!(
bd.get("relevance").is_some(),
"breakdown must have relevance field; got: {bd}"
);
assert!(
bd.get("temporal").is_some(),
"breakdown must have temporal field; got: {bd}"
);
}
#[tokio::test]
async fn recall_presentation_alias_is_rejected_by_deny_unknown_fields() {
let registry = make_registry(make_runtime());
let err = registry
.dispatch(
"memory.recall",
json!({ "query": "transformer", "presentation": "verbose" }),
)
.await
.expect_err("presentation alias must be rejected");
let msg = err.to_string();
assert!(
msg.contains("unknown field") && msg.contains("presentation"),
"error must mention unknown field 'presentation'; got: {msg}"
);
}
struct ConstVecService {
dims: usize,
seed: f32,
}
#[async_trait]
impl EmbeddingService for ConstVecService {
async fn embed(
&self,
texts: &[String],
_model: EmbeddingModel,
) -> std::result::Result<Vec<Vec<f32>>, EmbedError> {
Ok(texts.iter().map(|_| vec![self.seed; self.dims]).collect())
}
fn supports_model(&self, _model: EmbeddingModel) -> bool {
true
}
fn name(&self) -> &'static str {
"const-vec"
}
}
struct ConstVecProvider {
provider_name: String,
dims: usize,
seed: f32,
}
impl ConstVecProvider {
fn new(name: &str, dims: usize, seed: f32) -> Self {
Self {
provider_name: name.to_owned(),
dims,
seed,
}
}
}
#[async_trait]
impl EmbedderProvider for ConstVecProvider {
fn name(&self) -> &str {
&self.provider_name
}
fn dimensions(&self) -> usize {
self.dims
}
async fn build(&self) -> Result<Arc<dyn EmbeddingService>, khive_runtime::RuntimeError> {
Ok(Arc::new(ConstVecService {
dims: self.dims,
seed: self.seed,
}))
}
}
#[tokio::test]
async fn test_custom_embedder_only_runtime_fanout_remember_recall() {
const MODEL_A: &str = "custom-enc-a";
const DIMS: usize = 4;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
..RuntimeConfig::default()
})
.expect("runtime");
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.9));
assert!(rt.config().embedding_model.is_none());
assert!(
rt.registered_embedding_model_names()
.contains(&MODEL_A.to_string()),
"custom embedder must be in registry"
);
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "custom embedder fanout regression test content alpha",
"salience": 0.8
}),
)
.await
.expect("remember with custom-only embedder must succeed");
let note_id = result["id"].as_str().expect("note_id present");
assert!(!note_id.is_empty());
let recall_result = registry
.dispatch(
"memory.recall",
json!({ "query": "custom embedder fanout regression" }),
)
.await
.expect("recall after custom-embedder remember");
let hits = recall_result.as_array().expect("array");
let ids: Vec<&str> = hits.iter().map(|h| h["id"].as_str().unwrap()).collect();
assert!(
ids.contains(¬e_id),
"recall must find the note created via custom embedder; got: {ids:?}"
);
}
#[tokio::test]
async fn test_weighted_fusion_multi_model_text_not_zeroed() {
const MODEL_A: &str = "enc-model-a";
const MODEL_B: &str = "enc-model-b";
const DIMS: usize = 4;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
..RuntimeConfig::default()
})
.expect("runtime");
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.5));
rt.register_embedder(ConstVecProvider::new(MODEL_B, DIMS, 0.6));
assert_eq!(rt.registered_embedding_model_names().len(), 2);
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "weighted fusion multi model text contribution regression beta",
"salience": 0.7
}),
)
.await
.expect("remember with two custom embedders");
let note_id = result["id"].as_str().expect("note_id");
let recall = registry
.dispatch(
"memory.recall",
json!({
"query": "weighted fusion multi model text",
"fusion_strategy": "weighted",
"limit": 10
}),
)
.await
.expect("recall with weighted fusion and 2 vector models");
let hits = recall.as_array().expect("array");
let ids: Vec<&str> = hits.iter().map(|h| h["id"].as_str().unwrap()).collect();
assert!(
ids.contains(¬e_id),
"weighted fusion with N>1 vector models must not zero-weight text — \
note {note_id} must appear in results; got: {ids:?}"
);
}
#[tokio::test]
async fn test_remember_procedural_memory_type_rejected() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "procedural memory of how to deploy",
"memory_type": "procedural"
}),
)
.await;
assert!(
result.is_err(),
"memory_type='procedural' must be rejected; got ok: {result:?}"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("episodic") && msg.contains("semantic"),
"error must list valid memory_type values (episodic, semantic); got: {msg}"
);
}
#[tokio::test]
async fn test_recall_procedural_memory_type_filter_rejected() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.recall",
json!({
"query": "deploy procedure",
"memory_type": "procedural"
}),
)
.await;
assert!(
result.is_err(),
"recall with memory_type='procedural' must be rejected; got ok: {result:?}"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("episodic") && msg.contains("semantic"),
"error must list valid memory_type values; got: {msg}"
);
}
#[tokio::test]
async fn test_recall_composite_score_bounded_to_unit_interval() {
let rt = make_runtime();
let registry = make_registry(rt);
for i in 0..5 {
registry
.dispatch(
"memory.remember",
json!({
"content": format!("bounded score test memory number {i}"),
"salience": 0.5 + 0.1 * (i as f64),
"decay_factor": 0.0,
}),
)
.await
.expect("memory.remember");
}
let result = registry
.dispatch(
"memory.recall",
json!({ "query": "bounded score test memory", "limit": 10 }),
)
.await
.expect("recall succeeds");
let hits = result.as_array().expect("array of hits");
assert!(!hits.is_empty(), "must have hits for bounded score test");
for hit in hits {
let score = hit["score"].as_f64().expect("hit has score");
assert!(
(0.0..=1.0).contains(&score),
"composite score must be in [0, 1]; got {score}. \
If score > 1.0, normalize_relevance or weighted combination is broken."
);
}
}
#[test]
fn test_handler_def_min_score_description_clarified() {
use khive_types::Pack;
let recall_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.recall")
.expect("recall handler must be registered");
let min_score_param = recall_def
.params
.iter()
.find(|p| p.name == "min_score")
.expect("min_score param must exist");
assert!(
min_score_param.description.contains("composite")
|| min_score_param.description.contains("[0,1]"),
"min_score description must clarify the score is composite/[0,1]; got: {:?}",
min_score_param.description
);
}
#[test]
fn test_handler_def_remember_memory_type_description_lists_valid_values() {
use khive_types::Pack;
let remember_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.remember")
.expect("remember handler must be registered");
let mt_param = remember_def
.params
.iter()
.find(|p| p.name == "memory_type")
.expect("memory_type param must exist");
assert!(
mt_param.description.contains("episodic") && mt_param.description.contains("semantic"),
"memory_type description must list valid values 'episodic' and 'semantic'; got: {:?}",
mt_param.description
);
assert!(
!mt_param.description.contains("e.g."),
"memory_type description must not use 'e.g.' — values are exhaustive; got: {:?}",
mt_param.description
);
}
#[tokio::test]
async fn recall_candidates_text_candidates_non_empty_for_partial_match() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
serde_json::json!({
"content": "attention mechanism in neural networks",
"salience": 0.9,
"memory_type": "semantic"
}),
)
.await
.expect("remember succeeds");
let result = registry
.dispatch(
"memory.recall_candidates",
serde_json::json!({
"query": "attention mechanism transformers deep learning architecture"
}),
)
.await
.expect("recall_candidates succeeds");
let text_candidates = result["text_candidates"]
.as_array()
.expect("text_candidates is array");
assert!(
!text_candidates.is_empty(),
"text_candidates must be non-empty when a memory note partially matches the query; \
got empty array. Query fanout is likely not working."
);
for tc in text_candidates {
let note_id = tc["id"].as_str().expect("note_id present");
assert!(
!note_id.is_empty(),
"text_candidate note_id must be non-empty"
);
}
}
#[tokio::test]
async fn recall_include_breakdown_true_includes_breakdown() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
serde_json::json!({ "content": "breakdown test memory", "salience": 0.8 }),
)
.await
.expect("remember succeeds");
let result = registry
.dispatch(
"memory.recall",
serde_json::json!({ "query": "breakdown test memory", "include_breakdown": true }),
)
.await
.expect("recall with include_breakdown=true succeeds");
let hits = result.as_array().expect("array of hits");
assert!(!hits.is_empty(), "recall returned results");
assert!(
hits[0].get("breakdown").is_some(),
"include_breakdown=true must include 'breakdown' in results"
);
}
#[tokio::test]
async fn recall_default_omits_breakdown() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
serde_json::json!({ "content": "no breakdown memory", "salience": 0.8 }),
)
.await
.expect("remember succeeds");
let result = registry
.dispatch(
"memory.recall",
serde_json::json!({ "query": "no breakdown memory" }),
)
.await
.expect("recall without include_breakdown succeeds");
let hits = result.as_array().expect("array of hits");
if !hits.is_empty() {
assert!(
hits[0].get("breakdown").is_none(),
"default recall must not include 'breakdown' in results"
);
}
}
#[tokio::test]
async fn recall_handler_metadata_advertises_include_breakdown_not_presentation() {
let recall_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.recall")
.expect("memory.recall handler must be registered");
let has_include_breakdown = recall_def
.params
.iter()
.any(|p| p.name == "include_breakdown");
assert!(
has_include_breakdown,
"memory.recall must advertise include_breakdown param in metadata"
);
let has_presentation = recall_def.params.iter().any(|p| p.name == "presentation");
assert!(
!has_presentation,
"memory.recall must not advertise verb-level 'presentation' param to avoid ambiguity with MCP envelope"
);
}
#[tokio::test]
async fn search_kind_memory_resolves_when_memory_pack_loaded() {
let registry = make_registry(make_runtime());
assert!(
registry.all_note_kinds().contains(&"memory"),
"registry.all_note_kinds() must include \"memory\" when memory pack is loaded; got: {:?}",
registry.all_note_kinds()
);
let result = registry
.dispatch(
"search",
serde_json::json!({ "kind": "memory", "query": "test" }),
)
.await;
assert!(
result.is_ok(),
"search(kind=\"memory\") must succeed when memory pack is loaded; got: {:?}",
result.err()
);
}
#[tokio::test]
async fn recall_tags_filter_any_all_and_no_filter() {
let registry = make_registry(make_runtime());
let impl_khive = registry
.dispatch(
"memory.remember",
json!({
"content": "tag filter regression shared semantic target alpha",
"salience": 0.9,
"tags": ["role:implementer", "khive"]
}),
)
.await
.expect("remember impl khive");
let impl_khive_id = impl_khive["id"].as_str().unwrap().to_owned();
let critic_khive = registry
.dispatch(
"memory.remember",
json!({
"content": "tag filter regression shared semantic target beta",
"salience": 0.9,
"tags": ["role:critic", "khive"]
}),
)
.await
.expect("remember critic khive");
let critic_khive_id = critic_khive["id"].as_str().unwrap().to_owned();
let impl_rust = registry
.dispatch(
"memory.remember",
json!({
"content": "tag filter regression shared semantic target gamma",
"salience": 0.9,
"tags": ["role:implementer", "rust"]
}),
)
.await
.expect("remember impl rust");
let impl_rust_id = impl_rust["id"].as_str().unwrap().to_owned();
let no_filter = registry
.dispatch(
"memory.recall",
json!({ "query": "tag filter regression shared semantic target", "limit": 20 }),
)
.await
.expect("recall no filter");
let no_filter_ids: Vec<&str> = no_filter
.as_array()
.unwrap()
.iter()
.filter_map(|h| h["id"].as_str())
.collect();
assert!(
no_filter_ids.contains(&impl_khive_id.as_str()),
"no-filter must return impl+khive memory"
);
assert!(
no_filter_ids.contains(&critic_khive_id.as_str()),
"no-filter must return critic+khive memory"
);
assert!(
no_filter_ids.contains(&impl_rust_id.as_str()),
"no-filter must return impl+rust memory"
);
let any_result = registry
.dispatch(
"memory.recall",
json!({
"query": "tag filter regression shared semantic target",
"limit": 20,
"tags": ["role:critic", "rust"],
"tag_mode": "any"
}),
)
.await
.expect("recall tag any");
let any_ids: Vec<&str> = any_result
.as_array()
.unwrap()
.iter()
.filter_map(|h| h["id"].as_str())
.collect();
assert!(
any_ids.contains(&critic_khive_id.as_str()),
"any filter must include critic+khive (has role:critic)"
);
assert!(
any_ids.contains(&impl_rust_id.as_str()),
"any filter must include impl+rust (has rust)"
);
assert!(
!any_ids.contains(&impl_khive_id.as_str()),
"any filter must exclude impl+khive (has neither role:critic nor rust)"
);
let all_result = registry
.dispatch(
"memory.recall",
json!({
"query": "tag filter regression shared semantic target",
"limit": 20,
"tags": ["role:implementer", "khive"],
"tag_mode": "all"
}),
)
.await
.expect("recall tag all");
let all_ids: Vec<&str> = all_result
.as_array()
.unwrap()
.iter()
.filter_map(|h| h["id"].as_str())
.collect();
assert!(
all_ids.contains(&impl_khive_id.as_str()),
"all filter must include impl+khive (has both role:implementer and khive)"
);
assert!(
!all_ids.contains(&critic_khive_id.as_str()),
"all filter must exclude critic+khive (missing role:implementer)"
);
assert!(
!all_ids.contains(&impl_rust_id.as_str()),
"all filter must exclude impl+rust (missing khive)"
);
}
#[tokio::test]
async fn recall_raw_score_field_always_present_with_tag_filter() {
let registry = make_registry(make_runtime());
registry
.dispatch(
"memory.remember",
json!({
"content": "raw score presence check alpha beta gamma delta epsilon",
"salience": 0.9,
"tags": ["team:alpha", "project:khive"]
}),
)
.await
.expect("remember tagged memory");
registry
.dispatch(
"memory.remember",
json!({
"content": "raw score presence check alpha beta gamma delta epsilon",
"salience": 0.9,
"tags": ["team:alpha", "project:khive"]
}),
)
.await
.expect("remember second tagged memory");
for tag_mode in &["any", "all"] {
let result = registry
.dispatch(
"memory.recall",
json!({
"query": "raw score presence check alpha beta gamma",
"limit": 20,
"tags": ["team:alpha", "project:khive"],
"tag_mode": tag_mode
}),
)
.await
.unwrap_or_else(|e| panic!("recall tag_mode={tag_mode} failed: {e}"));
let hits = result.as_array().expect("results must be an array");
assert!(
!hits.is_empty(),
"tag_mode={tag_mode}: expected at least one result"
);
for (i, hit) in hits.iter().enumerate() {
let obj = hit.as_object().expect("each hit must be a JSON object");
assert!(
obj.contains_key("raw_score"),
"tag_mode={tag_mode} result[{i}] missing raw_score field; got keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
}
}
}
#[test]
fn recall_handler_metadata_advertises_tags_and_tag_mode() {
let recall_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.recall")
.expect("memory.recall handler must be registered");
let param_names: Vec<&str> = recall_def.params.iter().map(|p| p.name).collect();
assert!(
param_names.contains(&"tags"),
"memory.recall must advertise 'tags' param; got: {param_names:?}"
);
assert!(
param_names.contains(&"tag_mode"),
"memory.recall must advertise 'tag_mode' param; got: {param_names:?}"
);
}
#[tokio::test]
async fn recall_embed_default_omits_embedding_vectors() {
const MODEL_A: &str = "embed-a";
const DIMS: usize = 4;
let rt = make_runtime();
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.7));
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.recall_embed",
json!({ "query": "embedding metadata only" }),
)
.await
.expect("recall_embed default");
assert!(
result.get("embedding").is_none(),
"default recall_embed must not include top-level embedding; got: {result}"
);
assert_eq!(
result["dimensions"].as_u64(),
Some(DIMS as u64),
"dimensions must be returned even without embeddings"
);
let engines = result["engines"].as_array().expect("engines array");
assert_eq!(engines.len(), 1);
assert_eq!(engines[0]["model"].as_str(), Some(MODEL_A));
assert_eq!(engines[0]["dimensions"].as_u64(), Some(DIMS as u64));
assert!(
engines[0].get("embedding").is_none(),
"default recall_embed must not include per-engine embedding; got: {}",
engines[0]
);
}
#[tokio::test]
async fn recall_embed_include_embeddings_returns_vectors() {
const MODEL_A: &str = "embed-a";
const DIMS: usize = 4;
let rt = make_runtime();
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.7));
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.recall_embed",
json!({ "query": "embedding full payload", "include_embeddings": true }),
)
.await
.expect("recall_embed include embeddings");
let top_vec = result["embedding"]
.as_array()
.expect("top-level embedding array");
assert_eq!(
top_vec.len(),
DIMS,
"top-level embedding length must match dims"
);
let engines = result["engines"].as_array().expect("engines array");
assert_eq!(engines.len(), 1);
let engine_vec = engines[0]["embedding"]
.as_array()
.expect("per-engine embedding array");
assert_eq!(
engine_vec.len(),
DIMS,
"per-engine embedding length must match dims"
);
}
#[test]
fn recall_embed_handler_metadata_advertises_include_embeddings() {
let embed_def = khive_pack_memory::MemoryPack::HANDLERS
.iter()
.find(|h| h.name == "memory.recall_embed")
.expect("memory.recall_embed handler must be registered");
let param_names: Vec<&str> = embed_def.params.iter().map(|p| p.name).collect();
assert!(
param_names.contains(&"include_embeddings"),
"memory.recall_embed must advertise 'include_embeddings' param; got: {param_names:?}"
);
}
#[tokio::test]
async fn test_remember_episodic_defaults_stored() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({ "content": "episodic default test", "memory_type": "episodic" }),
)
.await
.expect("memory.remember must succeed");
let salience = result["salience"].as_f64().expect("salience field present");
let decay = result["decay_factor"]
.as_f64()
.expect("decay_factor field present");
assert!(
(salience - 0.3).abs() < 1e-12,
"episodic default salience must be 0.3, got {salience}"
);
assert!(
(decay - 0.02).abs() < 1e-12,
"episodic default decay_factor must be 0.02, got {decay}"
);
}
#[tokio::test]
async fn test_remember_omitted_memory_type_uses_episodic_defaults() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({ "content": "no memory_type supplied" }),
)
.await
.expect("memory.remember must succeed");
let mt = result["memory_type"].as_str().expect("memory_type present");
assert_eq!(
mt, "episodic",
"omitted memory_type must default to episodic"
);
let salience = result["salience"].as_f64().expect("salience present");
let decay = result["decay_factor"]
.as_f64()
.expect("decay_factor present");
assert!(
(salience - 0.3).abs() < 1e-12,
"omitted-type default salience must be 0.3, got {salience}"
);
assert!(
(decay - 0.02).abs() < 1e-12,
"omitted-type default decay_factor must be 0.02, got {decay}"
);
}
#[tokio::test]
async fn test_remember_semantic_defaults_stored() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({ "content": "semantic default test", "memory_type": "semantic" }),
)
.await
.expect("memory.remember must succeed");
let salience = result["salience"].as_f64().expect("salience present");
let decay = result["decay_factor"]
.as_f64()
.expect("decay_factor present");
assert!(
(salience - 0.5).abs() < 1e-12,
"semantic default salience must be 0.5, got {salience}"
);
assert!(
(decay - 0.005).abs() < 1e-12,
"semantic default decay_factor must be 0.005, got {decay}"
);
}
#[tokio::test]
async fn test_remember_explicit_salience_overrides_episodic_default() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({ "content": "explicit salience test", "memory_type": "episodic", "salience": 0.5 }),
)
.await
.expect("memory.remember must succeed");
let salience = result["salience"].as_f64().expect("salience present");
assert!(
(salience - 0.5).abs() < 1e-12,
"explicit salience=0.5 must be stored as-is, not replaced by episodic default 0.3; got {salience}"
);
}
#[tokio::test]
async fn test_remember_explicit_decay_overrides_episodic_default() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch(
"memory.remember",
json!({ "content": "explicit decay test", "memory_type": "episodic", "decay_factor": 0.01 }),
)
.await
.expect("memory.remember must succeed");
let decay = result["decay_factor"]
.as_f64()
.expect("decay_factor present");
assert!(
(decay - 0.01).abs() < 1e-12,
"explicit decay_factor=0.01 must be stored as-is, not replaced by episodic default 0.02; got {decay}"
);
}
#[tokio::test]
async fn test_recall_legacy_note_no_memory_type_returned_as_episodic() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let tok = rt.authorize(Namespace::local()).unwrap();
let legacy_note = rt
.create_note(
&tok,
"memory",
None,
"legacy note about transformer attention heads no memory type",
None, None, vec![], )
.await
.expect("create legacy note");
let legacy_id = legacy_note.id.to_string();
let result = registry
.dispatch(
"memory.recall",
json!({
"query": "transformer attention heads no memory type",
"memory_type": "episodic",
"limit": 10
}),
)
.await
.expect("memory.recall must succeed");
let hits = result.as_array().expect("recall returns array");
let returned_ids: Vec<&str> = hits
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
returned_ids.contains(&legacy_id.as_str()),
"legacy note with no stored memory_type must appear in recall(memory_type=\"episodic\"); \
returned ids: {returned_ids:?}"
);
let legacy_hit = hits
.iter()
.find(|h| h["id"].as_str().unwrap_or("") == legacy_id)
.expect("legacy hit present");
let hit_memory_type = legacy_hit["memory_type"]
.as_str()
.expect("memory_type field present in hit");
assert_eq!(
hit_memory_type, "episodic",
"recall hit memory_type must be resolved to \"episodic\" for legacy note; got {hit_memory_type:?}"
);
let hit_salience = legacy_hit["salience"]
.as_f64()
.expect("salience field present in hit");
assert!(
(hit_salience - 0.3).abs() < 1e-12,
"recall hit salience must be episodic default 0.3 for legacy note; got {hit_salience}"
);
let hit_decay = legacy_hit["decay_factor"]
.as_f64()
.expect("decay_factor field present in hit");
assert!(
(hit_decay - 0.02).abs() < 1e-12,
"recall hit decay_factor must be episodic default 0.02 for legacy note; got {hit_decay}"
);
}
#[tokio::test]
async fn test_recall_budget_capped_surfaces_signal() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let shared_query = "budget signal regression unique phrase alpha";
for i in 0..5_u8 {
let content =
format!("budget signal regression unique phrase alpha memory content item number {i}");
registry
.dispatch(
"memory.remember",
json!({ "content": content, "salience": 0.8 }),
)
.await
.expect("remember");
}
let result = registry
.dispatch(
"memory.recall",
json!({
"query": shared_query,
"limit": 5,
"config": {
"scoring": {
"default_token_budget": 20,
"chars_per_token": 4,
"mmr_penalty": 0.0
}
}
}),
)
.await
.expect("recall with tiny budget succeeds");
let returned = result
.as_array()
.expect("#94: recall must return a bare array on the non-verbose path");
assert!(
!returned.is_empty(),
"#94: at least one result must fit within the budget"
);
assert!(
returned.len() < 5,
"#94: returned count ({}) must be < requested limit (5) when budget is exhausted; \
prefix-cut is not working if all 5 fit in an 80-char budget",
returned.len()
);
}
#[tokio::test]
async fn test_recall_no_budget_cap_returns_plain_array() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
registry
.dispatch(
"memory.remember",
json!({ "content": "plain array uncapped recall test phrase", "salience": 0.7 }),
)
.await
.expect("remember");
let result = registry
.dispatch(
"memory.recall",
json!({ "query": "plain array uncapped recall test", "limit": 3 }),
)
.await
.expect("recall with generous budget succeeds");
assert!(
result.is_array(),
"#94 no-cap path: recall must return a plain array; got: {result}"
);
}
#[tokio::test]
async fn test_recall_budget_truncation_preserves_rank_order() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let query = "rankorder prefix budget truncation";
let content_rank1 = format!("{query} item short alpha unique");
let content_rank2 = format!("{query} item long beta {}", "x".repeat(50));
let content_rank3 = format!("{query} item short gamma unique");
let notes = [
(&content_rank1, 0.9_f64),
(&content_rank2, 0.6_f64),
(&content_rank3, 0.3_f64),
];
let mut stored_ids = Vec::new();
for (content, sal) in ¬es {
let res = registry
.dispatch(
"memory.remember",
json!({ "content": content, "salience": sal }),
)
.await
.expect("remember");
stored_ids.push(res["id"].as_str().unwrap().to_owned());
}
let id_rank1 = &stored_ids[0];
let id_rank3 = &stored_ids[2];
let result = registry
.dispatch(
"memory.recall",
json!({
"query": query,
"limit": 3,
"config": {
"scoring": {
"default_token_budget": 20,
"chars_per_token": 4,
"mmr_penalty": 0.0
}
}
}),
)
.await
.expect("recall with rank-order budget test");
let returned = result
.as_array()
.expect("#94 rank-order: recall must return a bare array");
assert_eq!(
returned.len(),
1,
"#94 rank-order: exactly 1 result expected (rank #1 fits, rank #2 overflows, \
rank #3 must not be reached); got {} — old retain bug returns [#1,#3]",
returned.len()
);
let returned_id = returned[0]["id"].as_str().expect("note_id present");
assert_eq!(
returned_id, id_rank1,
"#94 rank-order: the single returned note must be rank #1 ({id_rank1}), got {returned_id}"
);
let has_rank3 = returned.iter().any(|r| r["id"].as_str() == Some(id_rank3));
assert!(
!has_rank3,
"#94 rank-order: rank #3 ({id_rank3}) must NOT be in results — \
its presence proves the old greedy retain was used instead of the prefix cut"
);
}
#[tokio::test]
async fn adr007_rev4_writes_stamp_local() {
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![Namespace::parse("lambda:khive").unwrap()],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
let mut builder = VerbRegistryBuilder::new();
builder.with_visible_namespaces(rt.config().visible_namespaces.clone());
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "adr007 rev4 write test — must land in local",
"memory_type": "semantic",
"salience": 0.6
}),
)
.await
.expect("memory.remember must succeed");
let note_id = result["id"].as_str().expect("note id present");
let tok_local = rt.authorize(Namespace::local()).expect("authorize local");
let note = rt
.get_note_including_deleted(&tok_local, Uuid::parse_str(note_id).unwrap())
.await
.expect("get must succeed")
.expect("note must exist in local");
assert_eq!(
note.namespace, "local",
"write must stamp local, not actor namespace; got: {}",
note.namespace
);
}
#[tokio::test]
async fn adr007_rev4_no_actor_yields_local_only_visible_set() {
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
let tok_foreign = rt
.authorize(Namespace::parse("ns-foreign-c").unwrap())
.expect("authorize foreign");
rt.create_note(
&tok_foreign,
"memory",
None,
"adr007-c-foreign-content-unique-marker",
None,
None,
vec![],
)
.await
.expect("create note in foreign ns");
let mut builder = VerbRegistryBuilder::new();
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
let hits = registry
.dispatch(
"memory.recall",
json!({ "query": "adr007-c-foreign-content-unique-marker" }),
)
.await
.expect("memory.recall must succeed");
let hits = hits.as_array().expect("recall returns array");
let foreign_visible = hits.iter().any(|h| {
h["content"]
.as_str()
.unwrap_or("")
.contains("adr007-c-foreign-content-unique-marker")
});
assert!(
!foreign_visible,
"no-actor config: foreign-namespace note must NOT appear in default recall (visible-set={{local}} only)"
);
}
#[tokio::test]
async fn adr007_rev4_explicit_namespace_is_strict_reading2() {
const MODEL_A: &str = "custom-enc-a";
const DIMS: usize = 4;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![Namespace::parse("lambda:khive").unwrap()],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.9));
let tok_local = rt.authorize(Namespace::local()).expect("authorize local");
rt.create_note(
&tok_local,
"memory",
None,
"adr007-d-local-content-marker",
None,
None,
vec![],
)
.await
.expect("create local note");
let tok_lk = rt
.authorize(Namespace::parse("lambda:khive").unwrap())
.expect("authorize lambda:khive");
let lk_note = rt
.create_note(
&tok_lk,
"memory",
None,
"adr007-d-lambdakhive-content-marker",
None,
None,
vec![],
)
.await
.expect("create lambda:khive note");
let mut builder = VerbRegistryBuilder::new();
builder.with_visible_namespaces(rt.config().visible_namespaces.clone());
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
let results = registry
.dispatch(
"memory.recall",
json!({
"query": "adr007-d",
"namespace": "lambda:khive"
}),
)
.await
.expect("recall with explicit namespace must succeed");
let hits = results.as_array().expect("recall returns array");
let local_leaked = hits.iter().any(|h| {
h["content"]
.as_str()
.unwrap_or("")
.contains("adr007-d-local-content-marker")
});
assert!(
!local_leaked,
"Reading 2 violation: explicit namespace=lambda:khive must NOT return local-namespace notes; \
visible_namespaces must not widen explicit-namespace escapes"
);
let lk_found = hits.iter().any(|h| {
h["id"].as_str() == Some(&lk_note.id.to_string())
|| h["content"]
.as_str()
.unwrap_or("")
.contains("adr007-d-lambdakhive-content-marker")
});
assert!(
lk_found,
"explicit namespace=lambda:khive must return the lambda:khive note"
);
}
#[tokio::test]
async fn adr007_rev4_get_byid_is_namespace_agnostic() {
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
let tok_lk = rt
.authorize(Namespace::parse("lambda:khive").unwrap())
.expect("authorize lambda:khive");
let lk_note = rt
.create_note(
&tok_lk,
"memory",
None,
"adr007-e-byid-content",
None,
None,
vec![],
)
.await
.expect("create lambda:khive note");
let mut builder = VerbRegistryBuilder::new();
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
let result = registry
.dispatch("get", json!({ "id": lk_note.id.to_string() }))
.await
.expect("get by UUID must succeed regardless of namespace");
let record = result.get("record").unwrap_or(&result);
let returned_id = record["id"].as_str().expect("id present in get result");
assert_eq!(
returned_id,
lk_note.id.to_string().as_str(),
"by-ID get must return the lambda:khive-attributed note (Rule 2 / PR-A1 regression guard)"
);
}
#[tokio::test]
async fn adr007_rev4_default_recall_surfaces_actor_ns_via_both_legs() {
const MODEL_A: &str = "custom-enc-a";
const DIMS: usize = 4;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![Namespace::parse("lambda:khive").unwrap()],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.9));
let mut builder = VerbRegistryBuilder::new();
builder.with_visible_namespaces(rt.config().visible_namespaces.clone());
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "alpha beta gamma delta",
"salience": 0.8,
"namespace": "lambda:khive"
}),
)
.await
.expect("memory.remember with explicit namespace must succeed");
let note_id = result["id"].as_str().expect("note id present").to_owned();
assert!(!note_id.is_empty());
let recall_5a = registry
.dispatch("memory.recall", json!({ "query": "alpha beta" }))
.await
.expect("memory.recall (5a) must succeed");
let hits_5a = recall_5a.as_array().expect("recall returns array");
let ids_5a: Vec<&str> = hits_5a
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
ids_5a.contains(¬e_id.as_str()),
"5a: default recall must surface the lambda:khive note (both legs); got: {ids_5a:?}"
);
let recall_5b = registry
.dispatch("memory.recall", json!({ "query": "jxwvz qrpfk" }))
.await
.expect("memory.recall (5b) must succeed");
let hits_5b = recall_5b.as_array().expect("recall returns array");
let ids_5b: Vec<&str> = hits_5b
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
ids_5b.contains(¬e_id.as_str()),
"5b: vector-leg fanout via authorize(lambda:khive) must surface the note \
even with a lexically-disjoint query (no FTS match possible); got: {ids_5b:?}"
);
}
#[tokio::test]
async fn adr007_rev4_default_recall_surfaces_actor_ns_via_fts_leg() {
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![Namespace::parse("lambda:khive").unwrap()],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
assert!(
rt.registered_embedding_model_names().is_empty(),
"FTS-leg isolation premise: no embedder must be registered (vector leg must be dead)"
);
let mut builder = VerbRegistryBuilder::new();
builder.with_visible_namespaces(rt.config().visible_namespaces.clone());
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "alpha beta gamma delta",
"salience": 0.8,
"namespace": "lambda:khive"
}),
)
.await
.expect("memory.remember with explicit namespace must succeed");
let note_id = result["id"].as_str().expect("note id present").to_owned();
assert!(!note_id.is_empty());
let recall = registry
.dispatch("memory.recall", json!({ "query": "alpha beta" }))
.await
.expect("memory.recall must succeed");
let hits = recall.as_array().expect("recall returns array");
let ids: Vec<&str> = hits
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
ids.contains(¬e_id.as_str()),
"FTS-leg: default recall must surface the lambda:khive note via FTS fanout alone \
(vector leg is dead — no embedder registered); got: {ids:?}"
);
}
#[tokio::test]
async fn test_multi_namespace_recall_overfetch_filter() {
use khive_runtime::PackRuntime;
const MODEL: &str = "ns-filter-enc";
const DIMS: usize = 8;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
..RuntimeConfig::default()
})
.expect("runtime");
rt.register_embedder(ConstVecProvider::new(MODEL, DIMS, 0.42));
let registry = make_registry(rt.clone());
let mut local_ids = Vec::new();
for i in 0..3 {
let r = registry
.dispatch(
"memory.remember",
json!({
"content": format!("local memory entry number {i} about graph databases"),
"salience": 0.7,
"namespace": "local"
}),
)
.await
.unwrap_or_else(|e| panic!("local remember {i} failed: {e}"));
local_ids.push(r["id"].as_str().expect("note_id").to_string());
}
let mut other_ids = Vec::new();
for i in 0..2 {
let r = registry
.dispatch(
"memory.remember",
json!({
"content": format!("other namespace memory {i} about graph databases"),
"salience": 0.7,
"namespace": "other"
}),
)
.await
.unwrap_or_else(|e| panic!("other remember {i} failed: {e}"));
other_ids.push(r["id"].as_str().expect("note_id").to_string());
}
let default_recall = registry
.dispatch(
"memory.recall",
json!({ "query": "graph databases", "limit": 10 }),
)
.await
.expect("default recall");
let default_hits: Vec<&str> = default_recall
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap())
.collect();
for oid in &other_ids {
assert!(
!default_hits.contains(&oid.as_str()),
"default recall must exclude other-namespace memory {oid}; got: {default_hits:?}"
);
}
for lid in &local_ids {
assert!(
default_hits.contains(&lid.as_str()),
"default recall must include local memory {lid}; got: {default_hits:?}"
);
}
let pack = MemoryPack::new(rt.clone());
pack.warm().await;
let wide_token = rt
.authorize_with_visibility(
Namespace::parse("local").expect("local ns"),
vec![Namespace::parse("other").expect("other ns")],
)
.expect("wide token");
let wide_recall = pack
.dispatch(
"memory.recall",
json!({ "query": "graph databases", "limit": 10 }),
®istry,
&wide_token,
)
.await
.expect("wide-token recall");
let wide_hits: Vec<&str> = wide_recall
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap())
.collect();
for lid in &local_ids {
assert!(
wide_hits.contains(&lid.as_str()),
"wide-token recall must include local memory {lid}; got: {wide_hits:?}"
);
}
for oid in &other_ids {
assert!(
wide_hits.contains(&oid.as_str()),
"wide-token recall must include other-namespace memory {oid}; got: {wide_hits:?}"
);
}
let k3_recall = registry
.dispatch(
"memory.recall",
json!({ "query": "graph databases", "limit": 3 }),
)
.await
.expect("recall@3");
let k3_hits: Vec<&str> = k3_recall
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap())
.collect();
assert_eq!(
k3_hits.len(),
3,
"recall@3 must return exactly 3 results when 3 local candidates exist; got: {k3_hits:?}"
);
for lid in &local_ids {
assert!(
k3_hits.contains(&lid.as_str()),
"recall@3 must include local memory {lid}; got: {k3_hits:?}"
);
}
}
struct ClusteredVecService;
#[async_trait]
impl EmbeddingService for ClusteredVecService {
async fn embed(
&self,
texts: &[String],
_model: EmbeddingModel,
) -> std::result::Result<Vec<Vec<f32>>, EmbedError> {
Ok(texts
.iter()
.map(|t| {
if t.contains("xlocal") {
vec![0.15_f32, 0.9887, 0.0, 0.0]
} else {
vec![1.0_f32, 0.0, 0.0, 0.0]
}
})
.collect())
}
fn supports_model(&self, _model: EmbeddingModel) -> bool {
true
}
fn name(&self) -> &'static str {
"clustered-vec"
}
}
struct ClusteredVecProvider {
provider_name: String,
}
impl ClusteredVecProvider {
fn new(name: &str) -> Self {
Self {
provider_name: name.to_owned(),
}
}
}
#[async_trait]
impl EmbedderProvider for ClusteredVecProvider {
fn name(&self) -> &str {
&self.provider_name
}
fn dimensions(&self) -> usize {
4
}
async fn build(&self) -> Result<Arc<dyn EmbeddingService>, khive_runtime::RuntimeError> {
Ok(Arc::new(ClusteredVecService))
}
}
async fn c1_setup() -> (
KhiveRuntime,
khive_runtime::VerbRegistry,
MemoryPack,
Vec<String>,
) {
const MODEL: &str = "c1-clustered-enc";
const N_FOREIGN: usize = 50;
const N_LOCAL: usize = 3;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
..RuntimeConfig::default()
})
.expect("runtime");
rt.register_embedder(ClusteredVecProvider::new(MODEL));
let registry = make_registry(rt.clone());
for i in 0..N_FOREIGN {
registry
.dispatch(
"memory.remember",
json!({
"content": format!("xforeign memory slot {i} cluster-a"),
"salience": 0.6,
"namespace": "foreign"
}),
)
.await
.unwrap_or_else(|e| panic!("foreign remember {i} failed: {e}"));
}
let mut local_ids = Vec::new();
for i in 0..N_LOCAL {
let r = registry
.dispatch(
"memory.remember",
json!({
"content": format!("xlocal memory {i} must be recalled"),
"salience": 0.8,
"namespace": "local"
}),
)
.await
.unwrap_or_else(|e| panic!("local remember {i} failed: {e}"));
local_ids.push(r["id"].as_str().expect("note_id").to_string());
}
let pack = MemoryPack::new(rt.clone());
{
use khive_runtime::PackRuntime;
pack.warm().await;
}
(rt, registry, pack, local_ids)
}
#[tokio::test]
async fn test_ann_overfetch_retry_both_branches_deterministic() {
use khive_runtime::PackRuntime;
let (rt, registry, pack, local_ids) = c1_setup().await;
let local_token = rt
.authorize(Namespace::parse("local").expect("local ns"))
.expect("authorize local");
let result_with_retry = pack
.dispatch(
"memory.recall",
json!({
"query": "xforeign dominant cluster",
"limit": 3,
"config": {
"candidate_limit": 8,
"ann_overfetch_max_rounds": 3
}
}),
®istry,
&local_token,
)
.await
.expect("recall with retry");
let hits_with_retry: Vec<&str> = result_with_retry
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap())
.collect();
for lid in &local_ids {
assert!(
hits_with_retry.contains(&lid.as_str()),
"C1 branch-a: local memory {lid} MUST appear when retry rounds=3; \
got: {hits_with_retry:?}"
);
}
let result_no_retry = pack
.dispatch(
"memory.recall",
json!({
"query": "xforeign dominant cluster",
"limit": 3,
"config": {
"candidate_limit": 8,
"ann_overfetch_max_rounds": 1
}
}),
®istry,
&local_token,
)
.await
.expect("recall without retry");
let hits_no_retry: Vec<&str> = result_no_retry
.as_array()
.expect("array")
.iter()
.map(|h| h["id"].as_str().unwrap())
.collect();
for lid in &local_ids {
assert!(
!hits_no_retry.contains(&lid.as_str()),
"C1 branch-b: local memory {lid} must NOT appear when retry rounds=1 \
(corpus geometry broken if it does); got: {hits_no_retry:?}"
);
}
}
fn make_registry_with_actor(rt: KhiveRuntime, actor_id: &str) -> khive_runtime::VerbRegistry {
let mut builder = VerbRegistryBuilder::new();
builder.with_actor_id(Some(actor_id.to_string()));
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt));
builder.build().expect("registry with actor builds")
}
#[tokio::test]
async fn test_episodic_remember_stamps_actor_namespace() {
let rt = make_runtime();
let registry = make_registry_with_actor(rt.clone(), "alice");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "alice episodic event for namespace routing test",
"memory_type": "episodic",
}),
)
.await
.expect("memory.remember must succeed");
let note_id = result["id"].as_str().expect("response has id").to_owned();
let got = registry
.dispatch("get", json!({ "id": note_id }))
.await
.expect("get by UUID must succeed");
let stored_ns = got["namespace"].as_str().expect("note has namespace field");
assert_eq!(
stored_ns, "alice",
"ADR-007 Rev 6: episodic memory with actor_id=alice must be stamped in namespace 'alice'; \
got: {stored_ns:?}"
);
}
#[tokio::test]
async fn test_semantic_remember_stamps_local_namespace() {
let rt = make_runtime();
let registry = make_registry_with_actor(rt.clone(), "alice");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "alice semantic fact for namespace routing test",
"memory_type": "semantic",
}),
)
.await
.expect("memory.remember must succeed");
let note_id = result["id"].as_str().expect("response has id").to_owned();
let got = registry
.dispatch("get", json!({ "id": note_id }))
.await
.expect("get by UUID must succeed");
let stored_ns = got["namespace"].as_str().expect("note has namespace field");
assert_eq!(
stored_ns, "local",
"ADR-007 Rev 6: semantic memory must always land in 'local' regardless of actor; \
got: {stored_ns:?}"
);
}
#[tokio::test]
async fn test_remember_namespace_override_takes_precedence() {
let rt = make_runtime();
let registry = make_registry_with_actor(rt.clone(), "alice");
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "explicit override namespace test",
"memory_type": "episodic",
"namespace": "override-ns",
}),
)
.await
.expect("memory.remember with explicit namespace must succeed");
let note_id = result["id"].as_str().expect("response has id").to_owned();
let got = registry
.dispatch("get", json!({ "id": note_id }))
.await
.expect("get by UUID must succeed");
let stored_ns = got["namespace"].as_str().expect("note has namespace field");
assert_eq!(
stored_ns, "override-ns",
"ADR-007 Rev 6: explicit namespace= must override actor routing; \
got: {stored_ns:?}"
);
}
#[tokio::test]
async fn test_episodic_anonymous_actor_uses_local() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "anonymous episodic memory backward-compat test",
"memory_type": "episodic",
}),
)
.await
.expect("memory.remember must succeed");
let note_id = result["id"].as_str().expect("response has id").to_owned();
let got = registry
.dispatch("get", json!({ "id": note_id }))
.await
.expect("get by UUID must succeed");
let stored_ns = got["namespace"].as_str().expect("note has namespace field");
assert_eq!(
stored_ns, "local",
"ADR-007 Rev 6: anonymous actor (id='local') episodic memory must land in 'local' \
(backward-compat no-op); got: {stored_ns:?}"
);
}
#[tokio::test]
async fn adr007_rev6_episodic_cross_actor_isolation() {
const MODEL_A: &str = "rev6-isolation-enc";
const DIMS: usize = 4;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
embedding_model: None,
additional_embedding_models: vec![],
visible_namespaces: vec![Namespace::parse("alice").unwrap()],
..RuntimeConfig::default()
})
.expect("in-memory runtime");
rt.register_embedder(ConstVecProvider::new(MODEL_A, DIMS, 0.9));
let mut builder = VerbRegistryBuilder::new();
builder.with_actor_id(Some("alice".to_string()));
builder.with_visible_namespaces(vec![Namespace::parse("alice").unwrap()]);
builder.register(KgPack::new(rt.clone()));
builder.register(MemoryPack::new(rt.clone()));
let alice_registry = builder.build().expect("alice registry builds");
let anon_registry = make_registry(rt.clone());
let result = alice_registry
.dispatch(
"memory.remember",
json!({
"content": "alice private episodic cross-actor isolation marker",
"memory_type": "episodic",
}),
)
.await
.expect("memory.remember as alice must succeed");
let note_id = result["id"].as_str().expect("note id present").to_owned();
assert!(!note_id.is_empty());
let got = alice_registry
.dispatch("get", json!({ "id": note_id }))
.await
.expect("get by UUID must succeed");
let stored_ns = got["namespace"].as_str().expect("note has namespace field");
assert_eq!(
stored_ns, "alice",
"ADR-007 Rev 6: episodic memory must land in actor namespace 'alice'; got: {stored_ns:?}"
);
let alice_recall = alice_registry
.dispatch(
"memory.recall",
json!({ "query": "alice private episodic cross-actor isolation marker" }),
)
.await
.expect("memory.recall as alice must succeed");
let alice_hits = alice_recall.as_array().expect("recall returns array");
let alice_ids: Vec<&str> = alice_hits
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
alice_ids.contains(¬e_id.as_str()),
"ADR-007 Rev 6: alice must recall her own episodic memory; got: {alice_ids:?}"
);
let anon_recall = anon_registry
.dispatch(
"memory.recall",
json!({ "query": "alice private episodic cross-actor isolation marker" }),
)
.await
.expect("memory.recall as anonymous must succeed");
let anon_hits = anon_recall.as_array().expect("recall returns array");
let anon_ids: Vec<&str> = anon_hits
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
!anon_ids.contains(¬e_id.as_str()),
"ADR-007 Rev 6: anonymous principal (visible={{local}}) must NOT see alice's episodic \
memory (in namespace 'alice'); isolation FAILED — got: {anon_ids:?}"
);
}
#[tokio::test]
async fn test_expires_at_excluded_from_recall() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "this memory should expire and vanish from recall",
"memory_type": "semantic",
"salience": 0.9,
}),
)
.await
.expect("memory.remember must succeed");
let note_id_str = result["id"].as_str().expect("note id present").to_owned();
let note_id: Uuid = note_id_str.parse().expect("valid UUID");
let fresh_recall = registry
.dispatch(
"memory.recall",
json!({ "query": "memory should expire and vanish from recall" }),
)
.await
.expect("recall must succeed");
let fresh_hits = fresh_recall.as_array().expect("array");
let fresh_ids: Vec<&str> = fresh_hits
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
fresh_ids.contains(¬e_id_str.as_str()),
"memory must appear in recall before expiry; got: {fresh_ids:?}"
);
let past_micros = chrono::Utc::now().timestamp_micros() - 1_000_000; let tok = rt.authorize(Namespace::local()).expect("authorize local");
let note_store = rt.notes(&tok).expect("notes store");
let mut note = note_store
.get_note(note_id)
.await
.expect("get must succeed")
.expect("note must exist");
note.expires_at = Some(past_micros);
note_store
.upsert_note(note)
.await
.expect("upsert must succeed");
let expired_recall = registry
.dispatch(
"memory.recall",
json!({ "query": "memory should expire and vanish from recall" }),
)
.await
.expect("recall after expiry must succeed");
let expired_hits = expired_recall.as_array().expect("array");
let expired_ids: Vec<&str> = expired_hits
.iter()
.map(|h| h["id"].as_str().unwrap_or(""))
.collect();
assert!(
!expired_ids.contains(¬e_id_str.as_str()),
"expired memory (expires_at <= now) must be excluded from recall; got: {expired_ids:?}"
);
}
#[tokio::test]
async fn test_prune_by_salience() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let high = registry
.dispatch(
"memory.remember",
json!({
"content": "high salience memory that should survive prune",
"memory_type": "semantic",
"salience": 0.9,
}),
)
.await
.expect("remember high");
let high_id = high["id"].as_str().expect("id").to_owned();
let low = registry
.dispatch(
"memory.remember",
json!({
"content": "low salience memory that should be pruned",
"memory_type": "semantic",
"salience": 0.1,
}),
)
.await
.expect("remember low");
let low_id = low["id"].as_str().expect("id").to_owned();
let prune_result = registry
.dispatch("memory.prune", json!({ "min_salience": 0.5, "before": 0 }))
.await
.expect("memory.prune must succeed");
assert_eq!(
prune_result["dry_run"].as_bool(),
Some(false),
"dry_run must be false"
);
assert!(
prune_result["pruned"].as_u64().unwrap_or(0) >= 1,
"at least 1 memory pruned; got: {prune_result:?}"
);
let tok = rt.authorize(Namespace::local()).expect("authorize local");
let low_uuid: Uuid = low_id.parse().expect("valid uuid");
let low_note = rt
.get_note_including_deleted(&tok, low_uuid)
.await
.expect("get must succeed")
.expect("row must exist");
assert!(
low_note.deleted_at.is_some(),
"low-salience memory must be soft-deleted after prune; deleted_at={:?}",
low_note.deleted_at
);
let high_uuid: Uuid = high_id.parse().expect("valid uuid");
let high_note = rt
.get_note_including_deleted(&tok, high_uuid)
.await
.expect("get must succeed")
.expect("row must exist");
assert!(
high_note.deleted_at.is_none(),
"high-salience memory must NOT be deleted by prune; deleted_at={:?}",
high_note.deleted_at
);
}
#[tokio::test]
async fn test_prune_expired_by_default_cutoff() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "will be expired and then pruned",
"memory_type": "semantic",
"salience": 0.8,
}),
)
.await
.expect("remember");
let note_id_str = result["id"].as_str().expect("id").to_owned();
let note_id: Uuid = note_id_str.parse().expect("valid uuid");
let past_micros = chrono::Utc::now().timestamp_micros() - 1_000_000;
let tok = rt.authorize(Namespace::local()).expect("authorize local");
let note_store = rt.notes(&tok).expect("notes");
let mut note = note_store
.get_note(note_id)
.await
.expect("get")
.expect("exists");
note.expires_at = Some(past_micros);
note_store.upsert_note(note).await.expect("upsert");
let prune_result = registry
.dispatch("memory.prune", json!({}))
.await
.expect("memory.prune must succeed");
assert!(
prune_result["pruned"].as_u64().unwrap_or(0) >= 1,
"expired memory must be pruned; got: {prune_result:?}"
);
let pruned_note = rt
.get_note_including_deleted(&tok, note_id)
.await
.expect("get")
.expect("exists");
assert!(
pruned_note.deleted_at.is_some(),
"expired memory must be soft-deleted after prune"
);
}
#[tokio::test]
async fn test_prune_dry_run() {
let rt = make_runtime();
let registry = make_registry(rt.clone());
let result = registry
.dispatch(
"memory.remember",
json!({
"content": "dry run target memory",
"memory_type": "semantic",
"salience": 0.05,
}),
)
.await
.expect("remember");
let note_id_str = result["id"].as_str().expect("id").to_owned();
let note_id: Uuid = note_id_str.parse().expect("valid uuid");
let dry_result = registry
.dispatch(
"memory.prune",
json!({ "min_salience": 0.5, "before": 0, "dry_run": true }),
)
.await
.expect("memory.prune dry_run must succeed");
assert_eq!(
dry_result["dry_run"].as_bool(),
Some(true),
"dry_run field must be true"
);
assert!(
dry_result["would_prune"].as_u64().unwrap_or(0) >= 1,
"dry_run must report >= 1 would_prune; got: {dry_result:?}"
);
assert_eq!(
dry_result["pruned"].as_u64(),
Some(0),
"dry_run must not delete (pruned must be 0)"
);
let tok = rt.authorize(Namespace::local()).expect("authorize local");
let note = rt
.get_note_including_deleted(&tok, note_id)
.await
.expect("get")
.expect("exists");
assert!(
note.deleted_at.is_none(),
"dry_run must not soft-delete notes; deleted_at={:?}",
note.deleted_at
);
}
#[tokio::test]
async fn test_vacuum_succeeds() {
let rt = make_runtime();
let registry = make_registry(rt);
let result = registry
.dispatch("memory.vacuum", json!({}))
.await
.expect("memory.vacuum must succeed");
assert_eq!(
result["ok"].as_bool(),
Some(true),
"vacuum must return ok=true; got: {result:?}"
);
}