Initial commit: Vestige v1.0.0 - Cognitive memory MCP server

FSRS-6 spaced repetition, spreading activation, synaptic tagging,
hippocampal indexing, and 130 years of memory research.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-01-25 01:31:03 -06:00
commit f9c60eb5a7
169 changed files with 97206 additions and 0 deletions

View file

@ -0,0 +1,521 @@
//! Custom Test Assertions
//!
//! Provides domain-specific assertions for memory testing:
//! - Retention and decay assertions
//! - Scheduling assertions
//! - State transition assertions
//! - Search result assertions
use vestige_core::{KnowledgeNode, Storage};
// ============================================================================
// RETENTION ASSERTIONS
// ============================================================================
/// Assert that retention has decreased from an expected value
///
/// # Example
/// ```rust,ignore
/// assert_retention_decreased!(node.retention_strength, 1.0, 0.1);
/// ```
#[macro_export]
macro_rules! assert_retention_decreased {
($actual:expr, $original:expr) => {
assert!(
$actual < $original,
"Expected retention to decrease: {} should be less than {}",
$actual,
$original
);
};
($actual:expr, $original:expr, $min_decrease:expr) => {
let decrease = $original - $actual;
assert!(
decrease >= $min_decrease,
"Expected retention to decrease by at least {}: actual decrease was {} ({} -> {})",
$min_decrease,
decrease,
$original,
$actual
);
};
}
/// Assert that retention is within expected range
#[macro_export]
macro_rules! assert_retention_in_range {
($actual:expr, $min:expr, $max:expr) => {
assert!(
$actual >= $min && $actual <= $max,
"Expected retention in range [{}, {}], got {}",
$min,
$max,
$actual
);
};
}
/// Assert that retrieval strength has decayed properly
#[macro_export]
macro_rules! assert_retrieval_decayed {
($node:expr, $elapsed_days:expr) => {
let expected_max = 1.0; // Can't exceed 1.0
let expected_min = if $elapsed_days > 0.0 {
0.0 // Should have decayed at least somewhat
} else {
1.0
};
assert!(
$node.retrieval_strength >= expected_min && $node.retrieval_strength <= expected_max,
"Retrieval strength {} out of expected range [{}, {}] after {} days",
$node.retrieval_strength,
expected_min,
expected_max,
$elapsed_days
);
};
}
// ============================================================================
// SCHEDULING ASSERTIONS
// ============================================================================
/// Assert that a memory is due for review
#[macro_export]
macro_rules! assert_is_due {
($node:expr) => {
assert!(
$node.is_due(),
"Expected memory to be due for review, but next_review is {:?}",
$node.next_review
);
};
}
/// Assert that a memory is not due for review
#[macro_export]
macro_rules! assert_not_due {
($node:expr) => {
assert!(
!$node.is_due(),
"Expected memory to NOT be due for review, but it is (next_review: {:?})",
$node.next_review
);
};
}
/// Assert that interval increased after review
#[macro_export]
macro_rules! assert_interval_increased {
($before:expr, $after:expr) => {
let before_interval = $before
.next_review
.map(|t| (t - $before.last_accessed).num_days())
.unwrap_or(0);
let after_interval = $after
.next_review
.map(|t| (t - $after.last_accessed).num_days())
.unwrap_or(0);
assert!(
after_interval >= before_interval,
"Expected interval to increase: {} days -> {} days",
before_interval,
after_interval
);
};
}
/// Assert that stability increased after successful review
#[macro_export]
macro_rules! assert_stability_increased {
($before:expr, $after:expr) => {
assert!(
$after.stability >= $before.stability,
"Expected stability to increase: {} -> {}",
$before.stability,
$after.stability
);
};
}
// ============================================================================
// STATE ASSERTIONS
// ============================================================================
/// Assert that storage strength increased
#[macro_export]
macro_rules! assert_storage_strength_increased {
($before:expr, $after:expr) => {
assert!(
$after.storage_strength >= $before.storage_strength,
"Expected storage strength to increase: {} -> {}",
$before.storage_strength,
$after.storage_strength
);
};
}
/// Assert that reps count increased
#[macro_export]
macro_rules! assert_reps_increased {
($before:expr, $after:expr) => {
assert!(
$after.reps > $before.reps,
"Expected reps to increase: {} -> {}",
$before.reps,
$after.reps
);
};
}
/// Assert that lapses count increased
#[macro_export]
macro_rules! assert_lapses_increased {
($before:expr, $after:expr) => {
assert!(
$after.lapses > $before.lapses,
"Expected lapses to increase: {} -> {}",
$before.lapses,
$after.lapses
);
};
}
// ============================================================================
// TEMPORAL ASSERTIONS
// ============================================================================
/// Assert that a memory is currently valid
#[macro_export]
macro_rules! assert_currently_valid {
($node:expr) => {
assert!(
$node.is_currently_valid(),
"Expected memory to be currently valid, but valid_from={:?}, valid_until={:?}",
$node.valid_from,
$node.valid_until
);
};
}
/// Assert that a memory is not currently valid
#[macro_export]
macro_rules! assert_not_currently_valid {
($node:expr) => {
assert!(
!$node.is_currently_valid(),
"Expected memory to NOT be currently valid, but it is (valid_from={:?}, valid_until={:?})",
$node.valid_from,
$node.valid_until
);
};
}
/// Assert that a memory is valid at a specific time
#[macro_export]
macro_rules! assert_valid_at {
($node:expr, $time:expr) => {
assert!(
$node.is_valid_at($time),
"Expected memory to be valid at {:?}, but valid_from={:?}, valid_until={:?}",
$time,
$node.valid_from,
$node.valid_until
);
};
}
// ============================================================================
// SEARCH ASSERTIONS
// ============================================================================
/// Assert that search results contain a specific ID
#[macro_export]
macro_rules! assert_search_contains {
($results:expr, $id:expr) => {
assert!(
$results.iter().any(|n| n.id == $id),
"Expected search results to contain ID {}, but it was not found",
$id
);
};
}
/// Assert that search results do not contain a specific ID
#[macro_export]
macro_rules! assert_search_not_contains {
($results:expr, $id:expr) => {
assert!(
!$results.iter().any(|n| n.id == $id),
"Expected search results to NOT contain ID {}, but it was found",
$id
);
};
}
/// Assert search result count
#[macro_export]
macro_rules! assert_search_count {
($results:expr, $expected:expr) => {
assert_eq!(
$results.len(),
$expected,
"Expected {} search results, got {}",
$expected,
$results.len()
);
};
}
/// Assert that search results are ordered by relevance (first result is most relevant)
#[macro_export]
macro_rules! assert_search_order {
($results:expr, $expected_first:expr) => {
assert!(
!$results.is_empty(),
"Expected non-empty search results"
);
assert_eq!(
$results[0].id,
$expected_first,
"Expected first result to be {}, got {}",
$expected_first,
$results[0].id
);
};
}
// ============================================================================
// EMBEDDING ASSERTIONS
// ============================================================================
/// Assert that embeddings are similar (cosine similarity > threshold)
#[macro_export]
macro_rules! assert_embeddings_similar {
($emb1:expr, $emb2:expr, $threshold:expr) => {{
let dot: f32 = $emb1.iter().zip($emb2.iter()).map(|(a, b)| a * b).sum();
let norm1: f32 = $emb1.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm2: f32 = $emb2.iter().map(|x| x * x).sum::<f32>().sqrt();
let similarity = if norm1 > 0.0 && norm2 > 0.0 {
dot / (norm1 * norm2)
} else {
0.0
};
assert!(
similarity >= $threshold,
"Expected embeddings to be similar (>= {}), got similarity {}",
$threshold,
similarity
);
}};
}
/// Assert that embeddings are different (cosine similarity < threshold)
#[macro_export]
macro_rules! assert_embeddings_different {
($emb1:expr, $emb2:expr, $threshold:expr) => {{
let dot: f32 = $emb1.iter().zip($emb2.iter()).map(|(a, b)| a * b).sum();
let norm1: f32 = $emb1.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm2: f32 = $emb2.iter().map(|x| x * x).sum::<f32>().sqrt();
let similarity = if norm1 > 0.0 && norm2 > 0.0 {
dot / (norm1 * norm2)
} else {
0.0
};
assert!(
similarity < $threshold,
"Expected embeddings to be different (< {}), got similarity {}",
$threshold,
similarity
);
}};
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/// Verify that a node exists in storage
pub fn assert_node_exists(storage: &Storage, id: &str) {
let node = storage.get_node(id);
assert!(
node.is_ok() && node.unwrap().is_some(),
"Expected node {} to exist in storage",
id
);
}
/// Verify that a node does not exist in storage
pub fn assert_node_not_exists(storage: &Storage, id: &str) {
let node = storage.get_node(id);
assert!(
node.is_ok() && node.unwrap().is_none(),
"Expected node {} to NOT exist in storage",
id
);
}
/// Verify that storage has expected node count
pub fn assert_node_count(storage: &Storage, expected: i64) {
let stats = storage.get_stats().expect("Failed to get stats");
assert_eq!(
stats.total_nodes, expected,
"Expected {} nodes, got {}",
expected, stats.total_nodes
);
}
/// Verify that a node has the expected content
pub fn assert_node_content(node: &KnowledgeNode, expected_content: &str) {
assert_eq!(
node.content, expected_content,
"Expected content '{}', got '{}'",
expected_content, node.content
);
}
/// Verify that a node has the expected type
pub fn assert_node_type(node: &KnowledgeNode, expected_type: &str) {
assert_eq!(
node.node_type, expected_type,
"Expected type '{}', got '{}'",
expected_type, node.node_type
);
}
/// Verify that a node has specific tags
pub fn assert_has_tags(node: &KnowledgeNode, expected_tags: &[&str]) {
for tag in expected_tags {
assert!(
node.tags.contains(&tag.to_string()),
"Expected node to have tag '{}', but tags are {:?}",
tag,
node.tags
);
}
}
/// Verify difficulty is within valid range
pub fn assert_difficulty_valid(node: &KnowledgeNode) {
assert!(
node.difficulty >= 1.0 && node.difficulty <= 10.0,
"Difficulty {} is out of valid range [1.0, 10.0]",
node.difficulty
);
}
/// Verify stability is positive
pub fn assert_stability_valid(node: &KnowledgeNode) {
assert!(
node.stability > 0.0,
"Stability {} should be positive",
node.stability
);
}
/// Approximate equality for floating point
pub fn assert_approx_eq(actual: f64, expected: f64, epsilon: f64) {
assert!(
(actual - expected).abs() < epsilon,
"Expected {} to be approximately equal to {} (epsilon: {})",
actual,
expected,
epsilon
);
}
/// Approximate equality for f32
pub fn assert_approx_eq_f32(actual: f32, expected: f32, epsilon: f32) {
assert!(
(actual - expected).abs() < epsilon,
"Expected {} to be approximately equal to {} (epsilon: {})",
actual,
expected,
epsilon
);
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Duration, Utc};
fn create_test_node() -> KnowledgeNode {
let mut node = KnowledgeNode::default();
node.id = "test-id".to_string();
node.content = "test content".to_string();
node.node_type = "fact".to_string();
node.created_at = Utc::now();
node.updated_at = Utc::now();
node.last_accessed = Utc::now();
node.stability = 5.0;
node.difficulty = 5.0;
node.reps = 3;
node.lapses = 0;
node.storage_strength = 2.0;
node.retrieval_strength = 0.9;
node.retention_strength = 0.85;
node.sentiment_score = 0.0;
node.sentiment_magnitude = 0.0;
node.next_review = Some(Utc::now() + Duration::days(5));
node.source = None;
node.tags = vec!["test".to_string(), "example".to_string()];
node.valid_from = None;
node.valid_until = None;
node.has_embedding = None;
node.embedding_model = None;
node
}
#[test]
fn test_retention_assertions() {
assert_retention_decreased!(0.7, 1.0);
assert_retention_decreased!(0.5, 1.0, 0.3);
assert_retention_in_range!(0.85, 0.8, 0.9);
}
#[test]
fn test_scheduling_assertions() {
let mut node = create_test_node();
// Not due yet (next_review is in the future)
assert_not_due!(node);
// Make it due
node.next_review = Some(Utc::now() - Duration::hours(1));
assert_is_due!(node);
}
#[test]
fn test_temporal_assertions() {
let node = create_test_node();
assert_currently_valid!(node);
}
#[test]
fn test_helper_functions() {
let node = create_test_node();
assert_node_content(&node, "test content");
assert_node_type(&node, "fact");
assert_has_tags(&node, &["test", "example"]);
assert_difficulty_valid(&node);
assert_stability_valid(&node);
}
#[test]
fn test_approx_eq() {
assert_approx_eq(0.90001, 0.9, 0.001);
assert_approx_eq_f32(0.90001, 0.9, 0.001);
}
#[test]
fn test_embedding_assertions() {
let emb1 = vec![1.0f32, 0.0, 0.0];
let emb2 = vec![0.9, 0.1, 0.0];
let emb3 = vec![0.0, 1.0, 0.0];
assert_embeddings_similar!(emb1, emb2, 0.8);
assert_embeddings_different!(emb1, emb3, 0.5);
}
}