use std::sync::{Arc, Mutex, RwLock};
use serde_json::{Value, json};
use engram_core::config::Config;
use engram_core::dispatch;
use engram_core::indexes::IndexSet;
use engram_core::lock_helpers;
use engram_core::server::ServerState;
use engram_embeddings::Embedder;
use engram_llm_client::{EmbeddingProvider, TextGenerator};
use engram_router::Router;
use engram_storage::Database;
fn build_deterministic_state() -> Arc<ServerState> {
let database = Database::in_memory().expect("in-memory database");
let mut config = Config::default();
config.embedding.provider = "deterministic".into();
let indexes = IndexSet::new(|| config.build_hnsw_params()).expect("index set");
let embedder = Embedder::new(0);
let router = Router::new(0.1, 0.15);
let embedding_provider: Arc<dyn EmbeddingProvider + Send + Sync> = Arc::from(
config
.build_embedding_provider()
.expect("embedding provider"),
);
let text_generator: Option<Arc<dyn TextGenerator + Send + Sync>> =
config.build_text_generator().ok().map(Arc::from);
Arc::new(ServerState {
database: Mutex::new(database),
indexes: RwLock::new(indexes),
embedder,
router: Mutex::new(router),
config,
database_path: String::new(),
embedding_provider,
text_generator,
})
}
fn build_dimension_mismatch_state() -> Arc<ServerState> {
let database = Database::in_memory().expect("in-memory database");
let mut config = Config::default();
config.embedding.provider = "deterministic".into();
config.embedding.dimension = Some(16);
config.hnsw.dimension = 32;
let indexes = IndexSet::new(|| config.build_hnsw_params()).expect("index set");
let embedder = Embedder::new(0);
let router = Router::new(0.1, 0.15);
let embedding_provider: Arc<dyn EmbeddingProvider + Send + Sync> = Arc::from(
config
.build_embedding_provider()
.expect("embedding provider"),
);
let text_generator: Option<Arc<dyn TextGenerator + Send + Sync>> =
config.build_text_generator().ok().map(Arc::from);
Arc::new(ServerState {
database: Mutex::new(database),
indexes: RwLock::new(indexes),
embedder,
router: Mutex::new(router),
config,
database_path: String::new(),
embedding_provider,
text_generator,
})
}
fn make_store_params(marker: &str) -> Value {
json!({
"memory_type": "decision",
"context": format!("dedup context {marker}"),
"action": format!("dedup action {marker}"),
"result": format!("dedup result {marker}"),
})
}
fn make_distinct_store_params() -> Value {
json!({
"memory_type": "decision",
"context": "an entirely unrelated situation describing prolonged offline batch ingestion of \
historical climate telemetry gathered from remote arctic weather stations",
"action": "the operator reconfigured the distributed message broker partition assignment and \
rebalanced consumer groups to drain a multi-day processing backlog gradually",
"result": "throughput recovered to nominal levels and downstream dashboards reflected the \
corrected aggregates after the lengthy catch-up window finally completed cleanly",
})
}
fn make_shared_context_store_params(marker: &str) -> Value {
json!({
"memory_type": "decision",
"context": format!("dedup context {marker}"),
"action": "the operator reconfigured the distributed message broker partition assignment and \
rebalanced consumer groups to drain a multi-day processing backlog gradually",
"result": "throughput recovered to nominal levels and downstream dashboards reflected the \
corrected aggregates after the lengthy catch-up window finally completed cleanly",
})
}
async fn store(state: &Arc<ServerState>, params: Value) -> Value {
dispatch::route("memory_store", state, params)
.await
.expect("memory_store")
}
fn row_count(state: &Arc<ServerState>) -> usize {
lock_helpers::lock_db(state)
.list_all_memories()
.expect("list memories")
.len()
}
#[tokio::test]
async fn identical_memory_deduplicates_into_existing_row() {
let state = build_deterministic_state();
let first = store(&state, make_store_params("twin")).await;
let first_id = first["id"].as_str().expect("first id").to_string();
assert_eq!(
first["deduplicated"],
json!(false),
"the first store of a marker is never a duplicate"
);
assert_eq!(row_count(&state), 1, "first store inserts exactly one row");
let second = store(&state, make_store_params("twin")).await;
assert_eq!(
second["deduplicated"],
json!(true),
"a byte-identical second store must deduplicate"
);
assert_eq!(
second["merged_into"].as_str(),
Some(first_id.as_str()),
"the duplicate must merge into the original id"
);
assert_eq!(
second["id"].as_str(),
Some(first_id.as_str()),
"the returned id must be the surviving original"
);
assert_eq!(
row_count(&state),
1,
"deduplication must leave exactly one row (old always-insert path leaves two)"
);
let survivor = lock_helpers::lock_db(&state)
.get_memory(&first_id)
.expect("surviving row");
assert_eq!(
survivor.used_count, 1,
"touch_memory must increment the survivor's used_count to 1"
);
assert!(
survivor.last_used_at.is_some(),
"touch_memory must stamp last_used_at on the survivor"
);
}
#[tokio::test]
async fn distinct_memory_is_not_deduplicated() {
let state = build_deterministic_state();
let first = store(&state, make_store_params("alpha")).await;
let first_id = first["id"].as_str().expect("first id").to_string();
let second = store(&state, make_distinct_store_params()).await;
let second_id = second["id"].as_str().expect("second id").to_string();
assert_eq!(
second["deduplicated"],
json!(false),
"a fully distinct memory must not deduplicate"
);
assert!(
second.get("merged_into").is_none(),
"merged_into must be omitted when deduplicated is false"
);
assert_ne!(
first_id, second_id,
"distinct memories must receive distinct ids"
);
assert_eq!(
row_count(&state),
2,
"two distinct memories must produce two rows"
);
}
#[tokio::test]
async fn dedup_search_error_degrades_to_insert() {
let state = build_dimension_mismatch_state();
let embedding_dimension = state.embedding_provider.dimension();
let hnsw_dimension = state.config.hnsw.dimension;
assert_ne!(
embedding_dimension, hnsw_dimension,
"embedding and HNSW dimensions must differ to force a dedup search error"
);
let stored = store(&state, make_store_params("degraded")).await;
assert_eq!(
stored["deduplicated"],
json!(false),
"a failed dedup search must degrade to a normal non-dedup insert"
);
assert!(
stored["id"].as_str().is_some(),
"the store must still return a fresh memory id"
);
assert_eq!(
row_count(&state),
1,
"the degraded store must still persist the row"
);
}
#[tokio::test]
async fn shared_context_alone_does_not_deduplicate() {
let state = build_deterministic_state();
let first = store(&state, make_store_params("shared")).await;
let first_id = first["id"].as_str().expect("first id").to_string();
assert_eq!(row_count(&state), 1, "first store inserts exactly one row");
let second = store(&state, make_shared_context_store_params("shared")).await;
let second_id = second["id"].as_str().expect("second id").to_string();
assert_eq!(
second["deduplicated"],
json!(false),
"a context-only match must not deduplicate under the all-three-fields policy"
);
assert!(
second.get("merged_into").is_none(),
"merged_into must be omitted when deduplicated is false"
);
assert_ne!(
first_id, second_id,
"the non-merged memory must receive a distinct id"
);
assert_eq!(
row_count(&state),
2,
"matching context alone must still produce two rows"
);
}