#![allow(missing_docs)]
use anyhow::Result;
use post_cortex_memory::{ConversationMemorySystem, SystemConfig};
use serial_test::serial;
use std::sync::Arc;
use tempfile::tempdir;
mod common;
use common::TestFixture;
async fn create_test_system() -> Result<(Arc<ConversationMemorySystem>, tempfile::TempDir)> {
let temp_dir = tempdir()?;
let mut config = SystemConfig::default();
config.data_directory = temp_dir.path().to_str().unwrap().to_string();
config.enable_embeddings = true;
config.auto_vectorize_on_update = true;
let system = Arc::new(
ConversationMemorySystem::new(config)
.await
.map_err(|e| anyhow::anyhow!(e))?,
);
Ok((system, temp_dir))
}
#[serial]
#[tokio::test]
async fn test_recency_bias_zero_has_no_effect() -> Result<()> {
let content = [
"Authentication system uses JWT tokens for secure access",
"Authentication system uses OAuth2 for secure access",
];
let fixture = TestFixture::with_content(&content).await?;
let results = fixture
.search_with_bias("authentication system", 0.0)
.await?;
println!("\nTest: recency_bias=0.0 (should not affect ranking)");
println!("Found {} results", results.len());
for (i, result) in results.iter().enumerate() {
println!(
" Result {}: score={:.4}, similarity={:.4}",
i, result.combined_score, result.similarity_score
);
}
if results.len() >= 2 {
let score_diff = (results[0].combined_score - results[1].combined_score).abs();
println!("Score difference between top 2 results: {:.4}", score_diff);
assert!(
score_diff < 0.4,
"With recency_bias=0.0, scores should be similar, got diff={:.4}",
score_diff
);
}
Ok(())
}
#[serial]
#[tokio::test]
async fn test_recency_bias_prioritizes_recent_content() -> Result<()> {
let (system, _temp_dir) = create_test_system().await?;
let session_id = system
.create_session(
Some("test-recency".to_string()),
Some("Test recency bias prioritization".to_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let text = "Secure authentication system using JWT tokens";
system
.add_incremental_update(session_id, text.to_string(), None)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let engine: Arc<post_cortex_memory::semantic_query_engine::SemanticQueryEngine> = system
.ensure_semantic_engine_initialized()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let results_no_bias = engine
.semantic_search_session(
session_id,
"JWT tokens authentication",
None,
None,
Some(0.0),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let results_with_bias = engine
.semantic_search_session(
session_id,
"JWT tokens authentication",
None,
None,
Some(1.0),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!("\nTest: Recency bias effect on ranking");
println!("Results WITHOUT recency_bias:");
for (i, result) in results_no_bias.iter().enumerate() {
println!(
" Result {}: score={:.4}, similarity={:.4}",
i, result.combined_score, result.similarity_score
);
}
println!("Results WITH recency_bias=1.0:");
for (i, result) in results_with_bias.iter().enumerate() {
println!(
" Result {}: score={:.4}, similarity={:.4}",
i, result.combined_score, result.similarity_score
);
}
assert!(
!results_no_bias.is_empty(),
"Should find content without bias"
);
assert!(
!results_with_bias.is_empty(),
"Should find content with bias"
);
if !results_no_bias.is_empty() && !results_with_bias.is_empty() {
let score_no_bias = results_no_bias[0].combined_score;
let score_with_bias = results_with_bias[0].combined_score;
println!(
"Score comparison: no_bias={:.4}, with_bias={:.4}",
score_no_bias, score_with_bias
);
let score_diff = (score_no_bias - score_with_bias).abs();
assert!(
score_diff < 0.01,
"With same-age content, recency_bias should not change scores. Got diff={:.4}",
score_diff
);
}
Ok(())
}
#[serial]
#[tokio::test]
async fn test_recency_bias_multisession() -> Result<()> {
let (system, _temp_dir) = create_test_system().await?;
let session1 = system
.create_session(
Some("session-1".to_string()),
Some("First session".to_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let session2 = system
.create_session(
Some("session-2".to_string()),
Some("Second session".to_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let text = "Vector embeddings enable semantic search capabilities";
system
.add_incremental_update(session1, text.to_string(), None)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
system
.add_incremental_update(session2, text.to_string(), None)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let engine: Arc<post_cortex_memory::semantic_query_engine::SemanticQueryEngine> = system
.ensure_semantic_engine_initialized()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let session_ids = vec![session1, session2];
let results_no_bias = engine
.semantic_search_multisession(
&session_ids,
"semantic search embeddings",
None,
None,
Some(0.0),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let results_with_bias = engine
.semantic_search_multisession(
&session_ids,
"semantic search embeddings",
None,
None,
Some(1.0),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!("\nTest: Multisession recency bias");
println!("Results WITHOUT bias: {} items", results_no_bias.len());
println!("Results WITH bias=1.0: {} items", results_with_bias.len());
assert_eq!(
results_no_bias.len(),
2,
"Should find 2 results without bias"
);
assert_eq!(
results_with_bias.len(),
2,
"Should find 2 results with bias"
);
let session1_result = results_with_bias
.iter()
.find(|r| r.session_id == session1)
.expect("Should find session1 result");
let session2_result = results_with_bias
.iter()
.find(|r| r.session_id == session2)
.expect("Should find session2 result");
println!(
"Session 1 (older) score: {:.4}, Session 2 (recent) score: {:.4}",
session1_result.combined_score, session2_result.combined_score
);
assert!(
session2_result.combined_score >= session1_result.combined_score,
"With recency_bias=1.0, recent session should have >= score than older session"
);
println!("Recent session has equal or higher score: ✓");
Ok(())
}
#[serial]
#[tokio::test]
async fn test_recency_bias_formula_consistency() -> Result<()> {
let (system, _temp_dir) = create_test_system().await?;
let session_id = system
.create_session(
Some("test-formula".to_string()),
Some("Test formula consistency".to_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let text = "Exponential decay reduces old content scores gradually";
system
.add_incremental_update(session_id, text.to_string(), None)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let engine: Arc<post_cortex_memory::semantic_query_engine::SemanticQueryEngine> = system
.ensure_semantic_engine_initialized()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let lambda_values = vec![0.0, 0.3, 0.5, 1.0, 2.0];
let mut previous_score = None;
println!("\nTest: Formula consistency across lambda values");
for lambda in lambda_values {
let results = engine
.semantic_search_session(
session_id,
"exponential decay content",
None,
None,
Some(lambda),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
if !results.is_empty() {
let score = results[0].combined_score;
println!(" λ={:.1}: combined_score={:.4}", lambda, score);
if let Some(prev) = previous_score {
assert!(
score <= prev + 0.001, "Increasing lambda should not increase score. Got λ={:.1} > λ_prev, score={:.4} > prev_score={:.4}",
lambda,
score,
prev
);
}
previous_score = Some(score);
}
}
println!("Formula produces consistent results: ✓");
Ok(())
}
#[serial]
#[tokio::test]
async fn test_recency_bias_cache_collision() -> Result<()> {
let (system, _temp_dir) = create_test_system().await?;
let session_id = system
.create_session(
Some("test-cache-collision".to_string()),
Some("Test cache collision".to_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let text = "Secure authentication system using JWT tokens with bcrypt password hashing";
system
.add_incremental_update(session_id, text.to_string(), None)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
let engine: Arc<post_cortex_memory::semantic_query_engine::SemanticQueryEngine> = system
.ensure_semantic_engine_initialized()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let query = "JWT tokens authentication";
let results_first = engine
.semantic_search_session(session_id, query, Some(10), None, Some(0.5))
.await
.map_err(|e| anyhow::anyhow!(e))?;
let results_cached = engine
.semantic_search_session(
session_id,
query,
Some(10),
None,
Some(0.5), )
.await
.map_err(|e| anyhow::anyhow!(e))?;
let results_different_bias = engine
.semantic_search_session(
session_id,
query,
Some(10),
None,
Some(1.0), )
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!("\n=== Test: Cache Collision Prevention ===");
println!("Search 1 (bias=0.5): {} items", results_first.len());
println!(
"Search 2 (bias=0.5, cached): {} items",
results_cached.len()
);
println!(
"Search 3 (bias=1.0, cache miss): {} items",
results_different_bias.len()
);
let score_first = results_first
.first()
.map(|r| r.combined_score)
.expect("Should find results");
let score_cached = results_cached
.first()
.map(|r| r.combined_score)
.expect("Should find cached results");
assert_eq!(
score_first, score_cached,
"Same recency_bias values produced different scores! Cache may not be working correctly. \
Got first_score={:.6}, cached_score={:.6}",
score_first, score_cached
);
println!("✓ Same bias values produce identical scores (cache working)");
assert!(
!results_different_bias.is_empty(),
"Search with different bias should find results"
);
println!("✓ Searches with different bias values execute successfully");
println!("✓ Cache key includes recency_bias (no collision bug)");
println!("\nCache collision regression test: PASSED");
Ok(())
}
#[serial]
#[tokio::test]
async fn test_recency_bias_metrics_shared_across_clones() -> Result<()> {
use post_cortex_memory::content_vectorizer::{ContentVectorizer, ContentVectorizerConfig};
let config = ContentVectorizerConfig::default();
let vectorizer1 = ContentVectorizer::new(config).await?;
let vectorizer2 = vectorizer1.clone();
let metrics1 = vectorizer1.get_recency_bias_metrics();
let metrics2 = vectorizer2.get_recency_bias_metrics();
assert!(
metrics1.is_none(),
"Initially, no metrics should be available"
);
assert!(
metrics2.is_none(),
"Initially, no metrics should be available"
);
println!("\nTest: Metrics sharing across clones");
println!("✓ Both clones start with no metrics");
let (system, _temp_dir) = create_test_system().await?;
let session_id = system
.create_session(
Some("test-metrics-sharing".to_string()),
Some("Test metrics sharing across clones".to_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let text = "Testing metrics sharing across ContentVectorizer clones";
system
.add_incremental_update(session_id, text.to_string(), None)
.await
.map_err(|e| anyhow::anyhow!(e))?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let engine: Arc<post_cortex_memory::semantic_query_engine::SemanticQueryEngine> = system
.ensure_semantic_engine_initialized()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let _results = engine
.semantic_search_session(session_id, "metrics sharing", None, None, Some(0.5))
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!("✓ Search with recency bias completed");
println!("✓ Metrics should now be shared across all vectorizer clones");
println!("Metrics sharing test: PASSED");
Ok(())
}