use chrono::{Duration, Utc};
use std::time::Instant;
use tempfile::TempDir;
use shodh_memory::embeddings::ner::{NerConfig, NeuralNer};
use shodh_memory::memory::types::{
geohash_decode, geohash_encode, geohash_neighbors, geohash_precision_for_radius, Experience,
ExperienceType, GeoFilter, Query,
};
use shodh_memory::memory::{MemoryConfig, MemorySystem};
fn setup_fallback_ner() -> NeuralNer {
let config = NerConfig::default();
NeuralNer::new_fallback(config)
}
fn create_experience_with_ner(content: &str, ner: &NeuralNer) -> Experience {
let entities = ner.extract(content).unwrap_or_default();
let entity_names: Vec<String> = entities.iter().map(|e| e.text.clone()).collect();
Experience {
content: content.to_string(),
experience_type: ExperienceType::Observation,
entities: entity_names,
..Default::default()
}
}
fn create_test_config(temp_dir: &TempDir) -> MemoryConfig {
MemoryConfig {
storage_path: temp_dir.path().to_path_buf(),
working_memory_size: 100,
session_memory_size_mb: 50,
max_heap_per_user_mb: 500,
auto_compress: false,
compression_age_days: 30,
importance_threshold: 0.3,
}
}
fn create_experience(content: &str) -> Experience {
Experience {
content: content.to_string(),
experience_type: ExperienceType::Observation,
entities: vec!["test".to_string()],
..Default::default()
}
}
fn create_geo_experience(content: &str, lat: f64, lon: f64) -> Experience {
Experience {
content: content.to_string(),
experience_type: ExperienceType::Observation,
geo_location: Some([lat, lon, 0.0]),
entities: vec!["geo_test".to_string()],
..Default::default()
}
}
#[test]
fn test_bug001_multiple_memories_same_day_all_retrievable() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let mut memory_ids = Vec::new();
for i in 0..10 {
let exp = Experience {
content: format!("Memory {} created at the same moment", i),
experience_type: ExperienceType::Observation,
entities: vec![format!("entity_{}", i)],
..Default::default()
};
let id = system.remember(exp, None).expect("Failed to record memory");
memory_ids.push(id);
}
let stats = system.stats();
assert!(
stats.total_memories >= 10,
"BUG-001 REGRESSION: Expected at least 10 memories, got {}. \
Date index key collision causing overwrites!",
stats.total_memories
);
for i in 0..10 {
let query = Query {
query_text: Some(format!("Memory {} created", i)),
max_results: 5,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
assert!(
!results.is_empty(),
"BUG-001 REGRESSION: Memory {} not found. Date index overwrite!",
i
);
}
}
#[test]
fn test_bug001_date_search_returns_all_memories() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
for i in 0..5 {
let exp = create_experience(&format!("Date search test memory {}", i));
system.remember(exp, None).expect("Failed to record");
}
let today = Utc::now();
let query = Query {
time_range: Some((today - Duration::hours(1), today + Duration::hours(1))),
max_results: 100,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
assert!(
results.len() >= 5,
"BUG-001 REGRESSION: Date search returned {} memories, expected >= 5",
results.len()
);
}
#[test]
fn test_bug003_geohash_uses_10_char_precision() {
let lat = 37.7749;
let lon = -122.4194;
let hash = geohash_encode(lat, lon, 10);
assert_eq!(
hash.len(),
10,
"BUG-003 REGRESSION: Geohash should be 10 chars for 1.2m precision, got {} chars",
hash.len()
);
}
#[test]
fn test_bug003_geohash_distinguishes_close_points() {
let lat1 = 37.7749000;
let lon1 = -122.4194000;
let lat2 = 37.7749000;
let lon2 = -122.4193500;
let hash1 = geohash_encode(lat1, lon1, 10);
let hash2 = geohash_encode(lat2, lon2, 10);
assert_ne!(
hash1, hash2,
"BUG-003 REGRESSION: 10-char geohash should distinguish points 5m apart"
);
}
#[test]
fn test_bug003_geohash_encode_decode_accuracy() {
let lat = 37.7749;
let lon = -122.4194;
let hash = geohash_encode(lat, lon, 10);
let (decoded_lat, decoded_lon, _, _) = geohash_decode(&hash);
let lat_error = (lat - decoded_lat).abs();
let lon_error = (lon - decoded_lon).abs();
assert!(
lat_error < 0.00003,
"BUG-003 REGRESSION: Latitude error {} exceeds 2m threshold",
lat_error
);
assert!(
lon_error < 0.00003,
"BUG-003 REGRESSION: Longitude error {} exceeds 2m threshold",
lon_error
);
}
#[test]
fn test_bug005_index_removal_is_fast() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
for i in 0..100 {
let exp = create_experience(&format!("Index removal test memory {}", i));
system.remember(exp, None).expect("Failed to record");
}
let start = Instant::now();
system.flush_storage().expect("Failed to flush");
let duration = start.elapsed();
assert!(
duration.as_millis() < 500,
"BUG-005 REGRESSION: Index operations took {}ms, expected < 500ms. \
Possible O(n) scan regression!",
duration.as_millis()
);
}
#[test]
fn test_bug007_combined_search_scales_linearly() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
for i in 0..50 {
let exp = Experience {
content: format!("Combined search test memory {} with multiple criteria", i),
experience_type: ExperienceType::Observation,
entities: vec![
format!("entity_{}", i % 5),
format!("category_{}", i % 3),
"common_tag".to_string(),
],
robot_id: Some(format!("robot_{}", i % 10)),
..Default::default()
};
system.remember(exp, None).expect("Failed to record");
}
let start = Instant::now();
let query = Query {
query_text: Some("Combined search test".to_string()),
robot_id: Some("robot_0".to_string()),
max_results: 100,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
let duration = start.elapsed();
assert!(
duration.as_millis() < 500,
"BUG-007 REGRESSION: Combined search took {}ms, expected < 500ms. \
Possible O(n*m) regression!",
duration.as_millis()
);
}
#[test]
fn test_bug009_geohash_skips_invalid_chars() {
let valid_hash = geohash_encode(37.7749, -122.4194, 10);
let invalid_hash = format!("{}@#!", &valid_hash[..5]);
let (lat, lon, _, _) = geohash_decode(&invalid_hash);
assert!(
lat.abs() < 90.0,
"BUG-009 REGRESSION: Latitude {} is invalid after invalid char handling",
lat
);
assert!(
lon.abs() < 180.0,
"BUG-009 REGRESSION: Longitude {} is invalid after invalid char handling",
lon
);
}
#[test]
fn test_bug009_geohash_empty_string() {
let (lat, lon, _, _) = geohash_decode("");
assert!(
lat >= -90.0 && lat <= 90.0 && lon >= -180.0 && lon <= 180.0,
"BUG-009: Empty geohash should decode to valid coordinates, got ({}, {})",
lat,
lon
);
}
#[test]
fn test_bug009_geohash_all_invalid() {
let (lat, lon, _, _) = geohash_decode("@#$%^&*!");
assert!(
lat.abs() <= 90.0 && lon.abs() <= 180.0,
"BUG-009: All-invalid geohash should still produce valid coords"
);
}
#[test]
fn test_bug010_radius_nan_handled() {
let precision = geohash_precision_for_radius(f64::NAN);
assert!(
precision >= 1 && precision <= 12,
"BUG-010 REGRESSION: NaN radius produced invalid precision {}",
precision
);
}
#[test]
fn test_bug010_radius_infinity_handled() {
let precision = geohash_precision_for_radius(f64::INFINITY);
assert!(
precision >= 1 && precision <= 12,
"BUG-010 REGRESSION: Infinity radius produced invalid precision {}",
precision
);
}
#[test]
fn test_bug010_radius_negative_handled() {
let precision = geohash_precision_for_radius(-100.0);
assert!(
precision >= 1 && precision <= 12,
"BUG-010 REGRESSION: Negative radius produced invalid precision {}",
precision
);
}
#[test]
fn test_bug010_radius_zero_handled() {
let precision = geohash_precision_for_radius(0.0);
assert!(
precision >= 1 && precision <= 12,
"BUG-010 REGRESSION: Zero radius produced invalid precision {}",
precision
);
}
#[test]
fn test_bug010_radius_huge_handled() {
let precision = geohash_precision_for_radius(50_000_000.0);
assert!(
precision >= 1 && precision <= 12,
"BUG-010 REGRESSION: Huge radius produced invalid precision {}",
precision
);
}
#[test]
fn test_bug010_radius_normal_values() {
let test_cases = [
(1.0, 12), (10.0, 10), (100.0, 8), (1000.0, 6), (10000.0, 5), (100000.0, 4), (1000000.0, 2), ];
for (radius, expected_min_precision) in test_cases {
let precision = geohash_precision_for_radius(radius);
assert!(
precision >= 1 && precision <= 12,
"Radius {} produced invalid precision {}",
radius,
precision
);
}
}
#[test]
fn test_algo004_importance_index_updates_after_change() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let exp = Experience {
content: "Important discovery about neural networks".to_string(),
experience_type: ExperienceType::Discovery,
entities: vec!["neural_networks".to_string()],
..Default::default()
};
let memory_id = system.remember(exp, None).expect("Failed to record");
let query = Query {
query_text: Some("neural networks".to_string()),
max_results: 5,
..Default::default()
};
let initial_results = system.recall(&query).expect("Failed to retrieve");
assert!(
!initial_results.is_empty(),
"Memory should be retrievable initially"
);
let memory_ids = vec![memory_id.clone()];
let outcome = shodh_memory::memory::RetrievalOutcome::Helpful;
system
.reinforce_recall(&memory_ids, outcome)
.expect("Failed to reinforce");
let after_results = system
.recall(&query)
.expect("Failed to retrieve after reinforce");
assert!(
!after_results.is_empty(),
"ALGO-004 REGRESSION: Memory not found after importance change. \
Importance index not re-indexed!"
);
if let Some(mem) = after_results.first() {
assert!(
mem.importance() > 0.0,
"Memory importance should be positive after reinforcement"
);
}
}
#[test]
fn test_bug006_empty_query_returns_gracefully() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
for i in 0..5 {
let exp = create_experience(&format!("Test memory {}", i));
system.remember(exp, None).expect("Failed to record");
}
let query = Query {
query_text: Some("".to_string()),
max_results: 10,
..Default::default()
};
let results = system.recall(&query);
assert!(
results.is_ok(),
"BUG-006: Empty query should not crash: {:?}",
results.err()
);
}
#[test]
fn test_bug004_vamana_maintains_recall_after_inserts() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let topics = [
"machine learning",
"deep learning",
"neural networks",
"AI models",
];
for i in 0..20 {
let topic = topics[i % topics.len()];
let exp = Experience {
content: format!(
"{} research paper {} about transformers and attention",
topic, i
),
experience_type: ExperienceType::Learning,
entities: vec![topic.to_string()],
..Default::default()
};
system.remember(exp, None).expect("Failed to record");
}
let query = Query {
query_text: Some("machine learning transformers".to_string()),
max_results: 10,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
assert!(
results.len() >= 3,
"BUG-004 REGRESSION: Expected at least 3 results for similar query, got {}. \
Vamana graph quality may be degraded!",
results.len()
);
}
#[test]
fn test_geohash_neighbors_returns_valid_hashes() {
let center = geohash_encode(37.7749, -122.4194, 6);
let neighbors = geohash_neighbors(¢er);
assert!(
neighbors.len() >= 8,
"geohash_neighbors should return at least 8 hashes, got {}",
neighbors.len()
);
for neighbor in &neighbors {
assert_eq!(
neighbor.len(),
center.len(),
"Neighbor {} has wrong length",
neighbor
);
}
}
#[test]
fn test_geo_filter_retrieval() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let locations = [
("San Francisco downtown", 37.7749, -122.4194),
("San Francisco near", 37.7750, -122.4195), ("Oakland", 37.8044, -122.2712), ("Los Angeles", 34.0522, -118.2437), ];
for (name, lat, lon) in locations {
let exp = create_geo_experience(&format!("Location test: {}", name), lat, lon);
system.remember(exp, None).expect("Failed to record");
}
let query = Query {
query_text: Some("Location test".to_string()),
geo_filter: Some(GeoFilter::new(37.7749, -122.4194, 1000.0)),
max_results: 10,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
assert!(
results.len() >= 1 && results.len() <= 3,
"Geo filter should return 1-3 results within 1km, got {}",
results.len()
);
}
#[test]
fn test_sho48_forget_removes_from_semantic_search() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let exp = Experience {
content: "SHO48 test memory with unique quantum entanglement photonic crystal content"
.to_string(),
experience_type: ExperienceType::Discovery,
entities: vec!["quantum".to_string(), "photonic".to_string()],
..Default::default()
};
let memory_id = system.remember(exp, None).expect("Failed to record memory");
let query = Query {
query_text: Some("quantum entanglement photonic crystal".to_string()),
max_results: 10,
..Default::default()
};
let results_before = system.recall(&query).expect("Failed to retrieve");
assert!(
!results_before.is_empty(),
"Memory should be findable before forget"
);
let forget_result = system
.forget(shodh_memory::memory::ForgetCriteria::ById(
memory_id.clone(),
))
.expect("Failed to forget");
assert_eq!(forget_result, 1, "Should have forgotten 1 memory");
let results_after = system.recall(&query).expect("Failed to retrieve");
let found_deleted = results_after.iter().any(|m| m.id == memory_id);
assert!(
!found_deleted,
"SHO-48 REGRESSION: Forgotten memory still found in semantic search! \
Vector index not cleaned on delete."
);
}
#[test]
fn test_sho48_forget_updates_stats() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let exp = create_experience("Stats tracking test memory for SHO-48");
let memory_id = system.remember(exp, None).expect("Failed to record memory");
let stats_before = system.stats();
system
.forget(shodh_memory::memory::ForgetCriteria::ById(memory_id))
.expect("Failed to forget");
let stats_after = system.stats();
assert!(
stats_after.total_memories < stats_before.total_memories,
"SHO-48 REGRESSION: total_memories not decremented after forget"
);
}
#[test]
fn test_sho49_no_duplicates_in_retrieve() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = MemoryConfig {
storage_path: temp_dir.path().to_path_buf(),
working_memory_size: 100,
session_memory_size_mb: 50,
max_heap_per_user_mb: 500,
auto_compress: false,
compression_age_days: 30,
importance_threshold: 0.0, };
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let mut recorded_ids = std::collections::HashSet::new();
for i in 0..10 {
let exp = Experience {
content: format!("Important memory {} for deduplication test", i),
experience_type: ExperienceType::Decision, entities: vec!["dedup_test".to_string()],
..Default::default()
};
let id = system.remember(exp, None).expect("Failed to record");
recorded_ids.insert(id);
}
let query = Query {
max_results: 100,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
let mut seen_ids = std::collections::HashSet::new();
for memory in &results {
let is_new = seen_ids.insert(memory.id.clone());
assert!(
is_new,
"SHO-49 REGRESSION: Duplicate memory ID {:?} in results! \
Deduplication not working across tiers.",
memory.id
);
}
}
#[test]
fn test_sho49_retrieve_count_matches_unique() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
for i in 0..5 {
let exp = create_experience(&format!("Unique memory {} for count test", i));
system.remember(exp, None).expect("Failed to record");
}
let query = Query {
max_results: 100,
..Default::default()
};
let results = system.recall(&query).expect("Failed to retrieve");
let unique_count = results
.iter()
.map(|m| &m.id)
.collect::<std::collections::HashSet<_>>()
.len();
assert_eq!(
results.len(),
unique_count,
"SHO-49 REGRESSION: Result count ({}) != unique count ({}). Duplicates present!",
results.len(),
unique_count
);
}
#[test]
fn test_sho50_stats_updated_on_add() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let stats_before = system.stats();
let exp = create_experience("Stats test memory for SHO-50");
system.remember(exp, None).expect("Failed to record");
let stats_after = system.stats();
assert_eq!(
stats_after.total_memories,
stats_before.total_memories + 1,
"SHO-50 REGRESSION: total_memories not incremented on add"
);
assert!(
stats_after.working_memory_count > stats_before.working_memory_count,
"SHO-50 REGRESSION: working_memory_count not incremented on add"
);
assert!(
stats_after.long_term_memory_count > stats_before.long_term_memory_count,
"SHO-50 REGRESSION: long_term_memory_count not incremented on add"
);
assert!(
stats_after.vector_index_count > stats_before.vector_index_count,
"SHO-50 REGRESSION: vector_index_count not incremented on add"
);
}
#[test]
fn test_sho50_stats_updated_on_forget() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = create_test_config(&temp_dir);
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let exp = create_experience("Memory to forget for SHO-50 stats test");
let memory_id = system.remember(exp, None).expect("Failed to record");
let stats_before = system.stats();
system
.forget(shodh_memory::memory::ForgetCriteria::ById(memory_id))
.expect("Failed to forget");
let stats_after = system.stats();
assert!(
stats_after.total_memories < stats_before.total_memories,
"SHO-50 REGRESSION: total_memories not decremented on forget"
);
assert!(
stats_after.long_term_memory_count < stats_before.long_term_memory_count,
"SHO-50 REGRESSION: long_term_memory_count not decremented on forget"
);
}
#[test]
fn test_sho50_high_importance_updates_session_count() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = MemoryConfig {
storage_path: temp_dir.path().to_path_buf(),
working_memory_size: 100,
session_memory_size_mb: 50,
max_heap_per_user_mb: 500,
auto_compress: false,
compression_age_days: 30,
importance_threshold: 0.0, };
let system = MemorySystem::new(config, None).expect("Failed to create memory system");
let stats_before = system.stats();
let exp = Experience {
content: "Critical decision about system architecture".to_string(),
experience_type: ExperienceType::Decision, entities: vec!["architecture".to_string()],
..Default::default()
};
system.remember(exp, None).expect("Failed to record");
let stats_after = system.stats();
assert!(
stats_after.session_memory_count > stats_before.session_memory_count,
"SHO-50 REGRESSION: session_memory_count not incremented for high-importance memory"
);
}