Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
510
examples/ruvLLM/tests/integration.rs
Normal file
510
examples/ruvLLM/tests/integration.rs
Normal file
@@ -0,0 +1,510 @@
|
||||
//! Integration tests for RuvLLM
|
||||
//!
|
||||
//! Tests the complete pipeline from request to response.
|
||||
|
||||
use ruvllm::types::{EdgeType, Feedback, MemoryEdge, MemoryNode, NodeType};
|
||||
use ruvllm::{Config, Request, RuvLLM};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Atomic counter for unique test directories
|
||||
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Helper to create test config with unique database path
|
||||
fn test_config() -> Config {
|
||||
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
let db_path = format!("/tmp/ruvllm_test_{}.db", id);
|
||||
Config::builder()
|
||||
.db_path(&db_path)
|
||||
.embedding_dim(128)
|
||||
.router_hidden_dim(32)
|
||||
.learning_enabled(false)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_basic_query() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
let response = llm.query("What is machine learning?").await.unwrap();
|
||||
|
||||
assert!(!response.text.is_empty());
|
||||
assert!(!response.request_id.is_empty());
|
||||
assert!(response.confidence >= 0.0 && response.confidence <= 1.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_with_context() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
// Preload some context
|
||||
// (In real tests, we'd inject memory nodes)
|
||||
|
||||
let response = llm.query("Explain neural networks").await.unwrap();
|
||||
|
||||
assert!(!response.text.is_empty());
|
||||
assert!(response.latency.total_ms > 0.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_management() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
// Create a session
|
||||
let session = llm.new_session();
|
||||
assert!(!session.id.is_empty());
|
||||
|
||||
// Query with session
|
||||
let response = llm.query_session(&session, "Hello").await.unwrap();
|
||||
assert!(!response.text.is_empty());
|
||||
|
||||
// Query again in same session
|
||||
let response2 = llm
|
||||
.query_session(&session, "Follow up question")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!response2.text.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_routing_decision() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
let response = llm.query("Simple question").await.unwrap();
|
||||
|
||||
// Check routing info is populated
|
||||
assert!(response.routing_info.confidence >= 0.0);
|
||||
assert!(response.routing_info.temperature > 0.0);
|
||||
assert!(response.routing_info.top_p > 0.0);
|
||||
assert!(response.routing_info.context_size > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_latency_breakdown() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
let response = llm.query("Test query for latency").await.unwrap();
|
||||
|
||||
// All latency components should be non-negative
|
||||
assert!(response.latency.embedding_ms >= 0.0);
|
||||
assert!(response.latency.retrieval_ms >= 0.0);
|
||||
assert!(response.latency.routing_ms >= 0.0);
|
||||
assert!(response.latency.attention_ms >= 0.0);
|
||||
assert!(response.latency.generation_ms >= 0.0);
|
||||
|
||||
// Total should be sum of components (approximately)
|
||||
let sum = response.latency.embedding_ms
|
||||
+ response.latency.retrieval_ms
|
||||
+ response.latency.routing_ms
|
||||
+ response.latency.attention_ms
|
||||
+ response.latency.generation_ms;
|
||||
|
||||
// Allow some variance for overhead
|
||||
assert!(response.latency.total_ms >= sum * 0.9);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_feedback() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
let response = llm.query("Test for feedback").await.unwrap();
|
||||
|
||||
// Provide feedback
|
||||
let feedback = Feedback {
|
||||
request_id: response.request_id.clone(),
|
||||
rating: Some(5),
|
||||
correction: None,
|
||||
task_success: Some(true),
|
||||
};
|
||||
|
||||
// Should not error
|
||||
llm.feedback(feedback).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_queries() {
|
||||
let config = test_config();
|
||||
let llm = std::sync::Arc::new(RuvLLM::new(config).await.unwrap());
|
||||
|
||||
// Run multiple queries concurrently
|
||||
let mut handles = Vec::new();
|
||||
for i in 0..5 {
|
||||
let llm_clone = llm.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let query = format!("Concurrent query {}", i);
|
||||
llm_clone.query(query).await.unwrap()
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all
|
||||
for handle in handles {
|
||||
let response = handle.await.unwrap();
|
||||
assert!(!response.text.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shutdown() {
|
||||
let config = test_config();
|
||||
let llm = RuvLLM::new(config).await.unwrap();
|
||||
|
||||
// Query first
|
||||
llm.query("Before shutdown").await.unwrap();
|
||||
|
||||
// Shutdown should succeed
|
||||
llm.shutdown().await.unwrap();
|
||||
}
|
||||
|
||||
// Module-specific integration tests
|
||||
|
||||
mod memory_integration {
|
||||
use super::*;
|
||||
use ruvllm::config::MemoryConfig;
|
||||
use ruvllm::memory::MemoryService;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_memory_pipeline() {
|
||||
let config = MemoryConfig::default();
|
||||
let memory = MemoryService::new(&config).await.unwrap();
|
||||
|
||||
// Insert nodes
|
||||
let nodes: Vec<MemoryNode> = (0..100)
|
||||
.map(|i| {
|
||||
let mut vec: Vec<f32> = vec![0.0; 128];
|
||||
vec[i % 128] = 1.0;
|
||||
MemoryNode {
|
||||
id: format!("node-{}", i),
|
||||
vector: vec,
|
||||
text: format!("Document {} about topic {}", i, i % 10),
|
||||
node_type: NodeType::Document,
|
||||
source: "test".into(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for node in nodes {
|
||||
memory.insert_node(node).unwrap();
|
||||
}
|
||||
|
||||
// Insert edges
|
||||
for i in 0..99 {
|
||||
let edge = MemoryEdge {
|
||||
id: format!("edge-{}", i),
|
||||
src: format!("node-{}", i),
|
||||
dst: format!("node-{}", i + 1),
|
||||
edge_type: EdgeType::Follows,
|
||||
weight: 0.8,
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
memory.insert_edge(edge).unwrap();
|
||||
}
|
||||
|
||||
// Search
|
||||
let mut query = vec![0.0f32; 128];
|
||||
query[50] = 1.0;
|
||||
|
||||
let result = memory.search_with_graph(&query, 10, 64, 2).await.unwrap();
|
||||
|
||||
assert!(!result.candidates.is_empty());
|
||||
assert!(result.candidates.len() <= 10);
|
||||
|
||||
// First result should be close to node-50
|
||||
assert_eq!(result.candidates[0].id, "node-50");
|
||||
|
||||
// Subgraph should include neighbors
|
||||
assert!(!result.subgraph.nodes.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
mod router_integration {
|
||||
use super::*;
|
||||
use ruvllm::config::RouterConfig;
|
||||
use ruvllm::router::FastGRNNRouter;
|
||||
use ruvllm::types::RouterSample;
|
||||
|
||||
#[test]
|
||||
fn test_router_training_cycle() {
|
||||
let config = RouterConfig::default();
|
||||
let mut router = FastGRNNRouter::new(&config).unwrap();
|
||||
|
||||
// Create training samples
|
||||
let samples: Vec<RouterSample> = (0..100)
|
||||
.map(|i| RouterSample {
|
||||
features: vec![0.1; config.input_dim],
|
||||
label_model: i % 4,
|
||||
label_context: i % 5,
|
||||
label_temperature: 0.7,
|
||||
label_top_p: 0.9,
|
||||
quality: 0.8,
|
||||
latency_ms: 100.0 + (i as f32) * 10.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Train
|
||||
let metrics = router.train_batch(&samples, 0.001, 0.0, None, None);
|
||||
|
||||
assert!(metrics.total_loss >= 0.0);
|
||||
assert!(metrics.model_accuracy >= 0.0);
|
||||
|
||||
// Forward pass should work
|
||||
let features = vec![0.1; config.input_dim];
|
||||
let hidden = vec![0.0; config.hidden_dim];
|
||||
let decision = router.forward(&features, &hidden).unwrap();
|
||||
|
||||
assert!(decision.confidence >= 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_ewc() {
|
||||
let config = RouterConfig::default();
|
||||
let mut router = FastGRNNRouter::new(&config).unwrap();
|
||||
|
||||
// Initial training
|
||||
let samples1: Vec<RouterSample> = (0..50)
|
||||
.map(|_| RouterSample {
|
||||
features: vec![0.1; config.input_dim],
|
||||
label_model: 0,
|
||||
label_context: 0,
|
||||
label_temperature: 0.5,
|
||||
label_top_p: 0.9,
|
||||
quality: 0.9,
|
||||
latency_ms: 50.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
router.train_batch(&samples1, 0.001, 0.0, None, None);
|
||||
|
||||
// Compute Fisher information
|
||||
let fisher = router.compute_fisher(&samples1);
|
||||
|
||||
// Train on new task with EWC (using same weights as optimal for test)
|
||||
let samples2: Vec<RouterSample> = (0..50)
|
||||
.map(|_| RouterSample {
|
||||
features: vec![0.5; config.input_dim],
|
||||
label_model: 3,
|
||||
label_context: 4,
|
||||
label_temperature: 0.9,
|
||||
label_top_p: 0.95,
|
||||
quality: 0.7,
|
||||
latency_ms: 200.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Train with EWC regularization (using fisher as a proxy for optimal weights)
|
||||
let metrics = router.train_batch(
|
||||
&samples2,
|
||||
0.001,
|
||||
0.4,
|
||||
Some(&fisher),
|
||||
Some(&fisher), // Using fisher as placeholder for optimal weights
|
||||
);
|
||||
|
||||
// Total loss should be non-negative
|
||||
assert!(metrics.total_loss >= 0.0);
|
||||
assert!(metrics.samples_processed > 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod attention_integration {
|
||||
use super::*;
|
||||
use ruvllm::attention::GraphAttentionEngine;
|
||||
use ruvllm::config::EmbeddingConfig;
|
||||
use ruvllm::memory::SubGraph;
|
||||
|
||||
#[test]
|
||||
fn test_attention_with_complex_graph() {
|
||||
let config = EmbeddingConfig::default();
|
||||
let engine = GraphAttentionEngine::new(&config).unwrap();
|
||||
|
||||
// Create a complex subgraph
|
||||
let nodes: Vec<MemoryNode> = (0..20)
|
||||
.map(|i| {
|
||||
let mut vec = vec![0.1; config.dimension];
|
||||
vec[i % config.dimension] += 0.5;
|
||||
// Normalize
|
||||
let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
vec.iter_mut().for_each(|x| *x /= norm);
|
||||
|
||||
MemoryNode {
|
||||
id: format!("n-{}", i),
|
||||
vector: vec,
|
||||
text: format!("Node {}", i),
|
||||
node_type: NodeType::Document,
|
||||
source: "test".into(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create edges forming a more complex structure
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..19 {
|
||||
edges.push(MemoryEdge {
|
||||
id: format!("e-{}-{}", i, i + 1),
|
||||
src: format!("n-{}", i),
|
||||
dst: format!("n-{}", i + 1),
|
||||
edge_type: EdgeType::Follows,
|
||||
weight: 0.9,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
}
|
||||
// Add some cross-links
|
||||
for i in (0..15).step_by(5) {
|
||||
edges.push(MemoryEdge {
|
||||
id: format!("cross-{}", i),
|
||||
src: format!("n-{}", i),
|
||||
dst: format!("n-{}", i + 5),
|
||||
edge_type: EdgeType::SameTopic,
|
||||
weight: 0.7,
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let subgraph = SubGraph {
|
||||
nodes,
|
||||
edges,
|
||||
center_ids: vec!["n-0".into()],
|
||||
};
|
||||
|
||||
// Query
|
||||
let query = vec![0.2; config.dimension];
|
||||
let context = engine.attend(&query, &subgraph).unwrap();
|
||||
|
||||
// Validate
|
||||
assert_eq!(context.ranked_nodes.len(), 20);
|
||||
assert_eq!(context.attention_weights.len(), 20);
|
||||
|
||||
// Weights sum to 1
|
||||
let sum: f32 = context.attention_weights.iter().sum();
|
||||
assert!((sum - 1.0).abs() < 0.01);
|
||||
|
||||
// Multi-head weights
|
||||
assert!(!context.head_weights.is_empty());
|
||||
|
||||
// Summary stats
|
||||
assert_eq!(context.summary.num_nodes, 20);
|
||||
assert!(context.summary.num_edges > 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod embedding_integration {
|
||||
use super::*;
|
||||
use ruvllm::config::EmbeddingConfig;
|
||||
use ruvllm::embedding::{EmbeddingService, PoolingStrategy};
|
||||
|
||||
#[test]
|
||||
fn test_embedding_batch_processing() {
|
||||
let config = EmbeddingConfig::default();
|
||||
let service = EmbeddingService::new(&config).unwrap();
|
||||
|
||||
let texts: Vec<&str> = vec![
|
||||
"The quick brown fox",
|
||||
"Jumps over the lazy dog",
|
||||
"Machine learning is fascinating",
|
||||
"Neural networks process information",
|
||||
"Vector databases store embeddings",
|
||||
];
|
||||
|
||||
let embeddings = service.embed_batch(&texts).unwrap();
|
||||
|
||||
assert_eq!(embeddings.len(), 5);
|
||||
|
||||
// Check pairwise similarities
|
||||
let mut similarities = Vec::new();
|
||||
for i in 0..embeddings.len() {
|
||||
for j in (i + 1)..embeddings.len() {
|
||||
let dot: f32 = embeddings[i]
|
||||
.vector
|
||||
.iter()
|
||||
.zip(embeddings[j].vector.iter())
|
||||
.map(|(a, b)| a * b)
|
||||
.sum();
|
||||
similarities.push((i, j, dot));
|
||||
}
|
||||
}
|
||||
|
||||
// Related texts should have higher similarity
|
||||
// (In mock embeddings this may not hold, but structure should work)
|
||||
assert_eq!(similarities.len(), 10); // 5 choose 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_embedding_pooling_comparison() {
|
||||
let config = EmbeddingConfig::default();
|
||||
let service = EmbeddingService::new(&config).unwrap();
|
||||
|
||||
let text = "This is a test sentence for comparing pooling strategies";
|
||||
|
||||
let mean = service
|
||||
.embed_with_pooling(text, PoolingStrategy::Mean)
|
||||
.unwrap();
|
||||
let max = service
|
||||
.embed_with_pooling(text, PoolingStrategy::Max)
|
||||
.unwrap();
|
||||
let cls = service
|
||||
.embed_with_pooling(text, PoolingStrategy::CLS)
|
||||
.unwrap();
|
||||
let last = service
|
||||
.embed_with_pooling(text, PoolingStrategy::LastToken)
|
||||
.unwrap();
|
||||
|
||||
// All should produce valid embeddings
|
||||
for emb in [&mean, &max, &cls, &last] {
|
||||
let norm: f32 = emb.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
assert!((norm - 1.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
// CLS and Mean should differ
|
||||
let cls_mean_dot: f32 = cls
|
||||
.vector
|
||||
.iter()
|
||||
.zip(mean.vector.iter())
|
||||
.map(|(a, b)| a * b)
|
||||
.sum();
|
||||
assert!(cls_mean_dot.abs() < 0.999);
|
||||
}
|
||||
}
|
||||
|
||||
mod compression_integration {
|
||||
use super::*;
|
||||
use ruvllm::compression::CompressionService;
|
||||
use ruvllm::config::MemoryConfig;
|
||||
use ruvllm::memory::MemoryService;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compression_pipeline() {
|
||||
let config = MemoryConfig::default();
|
||||
let memory = MemoryService::new(&config).await.unwrap();
|
||||
|
||||
// Insert nodes
|
||||
for i in 0..50 {
|
||||
let node = MemoryNode {
|
||||
id: format!("compress-{}", i),
|
||||
vector: vec![0.1; 128],
|
||||
text: format!("Document {} for compression", i),
|
||||
node_type: NodeType::Document,
|
||||
source: "test".into(),
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
memory.insert_node(node).unwrap();
|
||||
}
|
||||
|
||||
// Create compression service
|
||||
let compression = CompressionService::new(5, 0.5);
|
||||
|
||||
// Run compression
|
||||
let stats = compression.run_compression(&memory).await.unwrap();
|
||||
|
||||
// Stats should be populated (even if 0 for mock)
|
||||
assert!(stats.clusters_found >= 0);
|
||||
}
|
||||
}
|
||||
800
examples/ruvLLM/tests/sona_integration.rs
Normal file
800
examples/ruvLLM/tests/sona_integration.rs
Normal file
@@ -0,0 +1,800 @@
|
||||
//! SONA Integration Tests
|
||||
//!
|
||||
//! Comprehensive end-to-end validation of SONA module components:
|
||||
//! - Full workflow from trajectory recording to LoRA application
|
||||
//! - Component integration (TrajectoryBuffer → ReasoningBank → LoRA)
|
||||
//! - Concurrent safety and thread-safe operations
|
||||
//! - Performance benchmarks for instant loop latency
|
||||
|
||||
use ruvllm::sona::engine::SonaEngineBuilder;
|
||||
use ruvllm::sona::*;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
// ============================================================================
|
||||
// Test 1: Full SONA Engine Workflow
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_full_sona_workflow() {
|
||||
// Create SONA engine with custom configuration
|
||||
let engine = SonaEngineBuilder::new()
|
||||
.hidden_dim(128)
|
||||
.micro_lora_rank(1)
|
||||
.base_lora_rank(8)
|
||||
.micro_lr(0.001)
|
||||
.base_lr(0.0001)
|
||||
.ewc_lambda(500.0)
|
||||
.pattern_clusters(10)
|
||||
.buffer_capacity(1000)
|
||||
.quality_threshold(0.5)
|
||||
.build();
|
||||
|
||||
assert!(engine.is_enabled());
|
||||
assert_eq!(engine.config().hidden_dim, 128);
|
||||
|
||||
// Start a trajectory
|
||||
let query_embedding = vec![0.5; 128];
|
||||
let mut builder = engine.begin_trajectory(query_embedding.clone());
|
||||
|
||||
// Record multiple steps
|
||||
builder.add_step(vec![0.6; 128], vec![0.3; 64], 0.7);
|
||||
builder.add_step(vec![0.7; 128], vec![0.4; 64], 0.8);
|
||||
builder.add_step(vec![0.8; 128], vec![0.5; 64], 0.9);
|
||||
|
||||
// End trajectory
|
||||
engine.end_trajectory(builder, 0.85);
|
||||
|
||||
// Verify trajectory was recorded
|
||||
let stats = engine.stats();
|
||||
assert_eq!(stats.trajectories_buffered, 1);
|
||||
|
||||
// Apply micro-LoRA to input vectors
|
||||
let input = vec![1.0; 128];
|
||||
let mut output = vec![0.0; 128];
|
||||
engine.apply_micro_lora(&input, &mut output);
|
||||
|
||||
// Flush instant learning updates
|
||||
engine.flush();
|
||||
|
||||
// Record more trajectories to trigger background learning
|
||||
for i in 0..150 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.1 * ((i % 10) as f32); 128]);
|
||||
builder.add_step(vec![0.5; 128], vec![0.4; 64], 0.8);
|
||||
builder.add_step(vec![0.6; 128], vec![0.5; 64], 0.85);
|
||||
engine.end_trajectory(builder, 0.8 + ((i % 5) as f32) * 0.02);
|
||||
}
|
||||
|
||||
// Run background learning cycle
|
||||
let result = engine.force_learn();
|
||||
assert!(
|
||||
result.contains("Forced learning:"),
|
||||
"Expected force_learn result message"
|
||||
);
|
||||
assert!(
|
||||
result.contains("trajectories"),
|
||||
"Expected trajectory count in result"
|
||||
);
|
||||
|
||||
// Verify patterns were extracted (may be 0 if quality threshold filters them out)
|
||||
let stats = engine.stats();
|
||||
println!("Patterns extracted: {}", stats.patterns_stored);
|
||||
|
||||
// Find similar patterns to query (may be empty if quality threshold filters patterns)
|
||||
let patterns = engine.find_patterns(&query_embedding, 5);
|
||||
|
||||
// Apply base-LoRA to layer output
|
||||
let layer_input = vec![1.0; 128];
|
||||
let mut layer_output = vec![0.0; 128];
|
||||
engine.apply_base_lora(0, &layer_input, &mut layer_output);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 2: TrajectoryBuffer → ReasoningBank Flow
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_trajectory_to_pattern_flow() {
|
||||
let engine = SonaEngine::new(256);
|
||||
|
||||
// Create clustered trajectories (two distinct groups)
|
||||
// Group A: High values in first half of embedding
|
||||
for i in 0..50 {
|
||||
let mut embedding = vec![0.0; 256];
|
||||
for j in 0..128 {
|
||||
embedding[j] = 0.8 + (i as f32 * 0.001);
|
||||
}
|
||||
|
||||
let mut builder = engine.begin_trajectory(embedding);
|
||||
builder.add_step(vec![0.9; 256], vec![], 0.85);
|
||||
builder.add_step(vec![0.95; 256], vec![], 0.9);
|
||||
engine.end_trajectory(builder, 0.88);
|
||||
}
|
||||
|
||||
// Group B: High values in second half of embedding
|
||||
for i in 0..50 {
|
||||
let mut embedding = vec![0.0; 256];
|
||||
for j in 128..256 {
|
||||
embedding[j] = 0.8 + (i as f32 * 0.001);
|
||||
}
|
||||
|
||||
let mut builder = engine.begin_trajectory(embedding);
|
||||
builder.add_step(vec![0.85; 256], vec![], 0.82);
|
||||
builder.add_step(vec![0.9; 256], vec![], 0.87);
|
||||
engine.end_trajectory(builder, 0.85);
|
||||
}
|
||||
|
||||
// Force background learning to extract patterns
|
||||
let result = engine.force_learn();
|
||||
assert!(
|
||||
result.contains("100 trajectories"),
|
||||
"Expected 100 trajectories processed"
|
||||
);
|
||||
|
||||
// Note: Patterns may not cluster perfectly into 2 groups due to:
|
||||
// - Quality threshold filtering
|
||||
// - K-means convergence behavior
|
||||
// - Minimum cluster size requirements
|
||||
let stats = engine.stats();
|
||||
// Just verify some patterns were extracted
|
||||
println!("Patterns extracted: {}", stats.patterns_stored);
|
||||
|
||||
// Test pattern retrieval (may be empty if quality filtering removes patterns)
|
||||
let mut query_a = vec![0.0; 256];
|
||||
for j in 0..128 {
|
||||
query_a[j] = 0.85;
|
||||
}
|
||||
let patterns_a = engine.find_patterns(&query_a, 3);
|
||||
println!("Patterns for query A: {}", patterns_a.len());
|
||||
|
||||
let mut query_b = vec![0.0; 256];
|
||||
for j in 128..256 {
|
||||
query_b[j] = 0.85;
|
||||
}
|
||||
let patterns_b = engine.find_patterns(&query_b, 3);
|
||||
println!("Patterns for query B: {}", patterns_b.len());
|
||||
|
||||
// The test validates the full workflow - pattern extraction may yield 0 patterns
|
||||
// if quality threshold filters them out, which is expected behavior
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 3: Learning Signals → MicroLoRA Gradient Accumulation
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_learning_signal_to_microlora() {
|
||||
let engine = SonaEngine::new(64);
|
||||
|
||||
// Generate learning signals through trajectories
|
||||
for i in 0..10 {
|
||||
let quality = 0.7 + (i as f32 * 0.02);
|
||||
let mut builder = engine.begin_trajectory(vec![0.5; 64]);
|
||||
|
||||
// Add steps with varying rewards
|
||||
builder.add_step(vec![0.6; 64], vec![], 0.7);
|
||||
builder.add_step(vec![0.7; 64], vec![], 0.8);
|
||||
builder.add_step(vec![0.8; 64], vec![], 0.9);
|
||||
|
||||
engine.end_trajectory(builder, quality);
|
||||
}
|
||||
|
||||
// Flush to apply accumulated gradients
|
||||
engine.flush();
|
||||
|
||||
// Test that micro-LoRA has been updated
|
||||
let input = vec![1.0; 64];
|
||||
let mut output_before = vec![0.0; 64];
|
||||
let mut output_after = vec![0.0; 64];
|
||||
|
||||
// Get baseline output
|
||||
engine.apply_micro_lora(&input, &mut output_before);
|
||||
|
||||
// Add more learning signals
|
||||
for _i in 0..20 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.6; 64]);
|
||||
builder.add_step(vec![0.7; 64], vec![], 0.85);
|
||||
builder.add_step(vec![0.8; 64], vec![], 0.9);
|
||||
engine.end_trajectory(builder, 0.88);
|
||||
}
|
||||
engine.flush();
|
||||
|
||||
// Get updated output
|
||||
engine.apply_micro_lora(&input, &mut output_after);
|
||||
|
||||
// Verify that LoRA output has changed (learning occurred)
|
||||
let diff: f32 = output_before
|
||||
.iter()
|
||||
.zip(&output_after)
|
||||
.map(|(a, b)| (a - b).abs())
|
||||
.sum();
|
||||
|
||||
// With enough learning signals, there should be measurable change
|
||||
assert!(diff > 0.0, "Expected LoRA weights to change after learning");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 4: EWC++ Task Boundary Detection
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_ewc_task_boundary_detection() {
|
||||
let engine = SonaEngineBuilder::new()
|
||||
.hidden_dim(128)
|
||||
.ewc_lambda(1000.0)
|
||||
.build();
|
||||
|
||||
// Task 1: Low-value embeddings (simulate one type of query)
|
||||
for i in 0..60 {
|
||||
let embedding = vec![0.1 + (i as f32 * 0.001); 128];
|
||||
let mut builder = engine.begin_trajectory(embedding);
|
||||
builder.add_step(vec![0.2; 128], vec![], 0.7);
|
||||
builder.add_step(vec![0.3; 128], vec![], 0.75);
|
||||
engine.end_trajectory(builder, 0.72);
|
||||
}
|
||||
|
||||
let result1 = engine.force_learn();
|
||||
let stats1 = engine.stats();
|
||||
let ewc_tasks_1 = stats1.ewc_tasks;
|
||||
|
||||
// Task 2: High-value embeddings (simulate different type of query)
|
||||
for i in 0..60 {
|
||||
let embedding = vec![0.8 + (i as f32 * 0.001); 128];
|
||||
let mut builder = engine.begin_trajectory(embedding);
|
||||
builder.add_step(vec![0.85; 128], vec![], 0.9);
|
||||
builder.add_step(vec![0.9; 128], vec![], 0.92);
|
||||
engine.end_trajectory(builder, 0.91);
|
||||
}
|
||||
|
||||
let result2 = engine.force_learn();
|
||||
let stats2 = engine.stats();
|
||||
let ewc_tasks_2 = stats2.ewc_tasks;
|
||||
|
||||
// Task boundary should be detected due to distribution shift
|
||||
// EWC task count should increase if boundary was detected
|
||||
assert!(
|
||||
ewc_tasks_2 >= ewc_tasks_1,
|
||||
"Expected EWC to track task progression"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 5: LoRA Engine - MicroLoRA + BaseLoRA Integration
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_lora_engine_integration() {
|
||||
let mut engine = LoRAEngine::new(64, 1, 8, 6);
|
||||
|
||||
assert!(engine.micro_enabled);
|
||||
assert!(engine.base_enabled);
|
||||
|
||||
// Create learning signals
|
||||
for _ in 0..10 {
|
||||
let signal = LearningSignal::with_gradient(vec![0.1; 64], vec![0.5; 64], 0.85);
|
||||
engine.accumulate_micro(&signal);
|
||||
}
|
||||
|
||||
// Apply micro updates
|
||||
engine.apply_micro(0.001);
|
||||
|
||||
// Test forward pass with both tiers
|
||||
let input = vec![1.0; 64];
|
||||
let mut output = vec![0.0; 64];
|
||||
|
||||
for layer_idx in 0..6 {
|
||||
engine.forward(layer_idx, &input, &mut output);
|
||||
}
|
||||
|
||||
// Verify output was modified by at least one tier
|
||||
let sum: f32 = output.iter().map(|x| x.abs()).sum();
|
||||
// With accumulated gradients, there should be non-zero output
|
||||
assert!(sum > 0.0, "Expected LoRA to modify output");
|
||||
|
||||
// Test disabling tiers
|
||||
engine.micro_enabled = false;
|
||||
let mut output_no_micro = vec![0.0; 64];
|
||||
engine.forward(0, &input, &mut output_no_micro);
|
||||
|
||||
engine.micro_enabled = true;
|
||||
engine.base_enabled = false;
|
||||
let mut output_no_base = vec![0.0; 64];
|
||||
engine.forward(0, &input, &mut output_no_base);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 6: Concurrent Trajectory Recording
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_trajectory_recording() {
|
||||
let engine = Arc::new(SonaEngine::new(128));
|
||||
let num_threads = 8;
|
||||
let trajectories_per_thread = 50;
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for thread_id in 0..num_threads {
|
||||
let engine_clone = Arc::clone(&engine);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
for i in 0..trajectories_per_thread {
|
||||
let embedding = vec![0.1 * ((thread_id * 100 + i) as f32 % 10.0); 128];
|
||||
let mut builder = engine_clone.begin_trajectory(embedding);
|
||||
|
||||
builder.add_step(vec![0.5; 128], vec![], 0.8);
|
||||
builder.add_step(vec![0.6; 128], vec![], 0.85);
|
||||
builder.add_step(vec![0.7; 128], vec![], 0.9);
|
||||
|
||||
engine_clone.end_trajectory(builder, 0.85);
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
for handle in handles {
|
||||
handle.join().expect("Thread panicked");
|
||||
}
|
||||
|
||||
// Verify all trajectories were recorded
|
||||
let stats = engine.stats();
|
||||
let expected = num_threads * trajectories_per_thread;
|
||||
|
||||
// Account for potential buffer overflow in high-concurrency scenarios
|
||||
assert!(
|
||||
stats.trajectories_buffered > 0,
|
||||
"Expected trajectories to be recorded"
|
||||
);
|
||||
assert!(
|
||||
stats.trajectories_buffered <= expected,
|
||||
"Buffered count should not exceed total submitted"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 7: Concurrent LoRA Applications
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_lora_application() {
|
||||
let engine = Arc::new(SonaEngine::new(64));
|
||||
|
||||
// Pre-populate with some learning
|
||||
for _i in 0..20 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.5; 64]);
|
||||
builder.add_step(vec![0.6; 64], vec![], 0.8);
|
||||
engine.end_trajectory(builder, 0.82);
|
||||
}
|
||||
engine.flush();
|
||||
|
||||
let num_threads = 4;
|
||||
let applications_per_thread = 100;
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for _ in 0..num_threads {
|
||||
let engine_clone = Arc::clone(&engine);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let input = vec![1.0; 64];
|
||||
let mut output = vec![0.0; 64];
|
||||
|
||||
for _ in 0..applications_per_thread {
|
||||
output.fill(0.0);
|
||||
engine_clone.apply_micro_lora(&input, &mut output);
|
||||
|
||||
// Verify output is valid
|
||||
assert!(!output.iter().any(|x| x.is_nan()));
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads
|
||||
for handle in handles {
|
||||
handle
|
||||
.join()
|
||||
.expect("Thread panicked during LoRA application");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 8: Thread-Safe Learning Signal Processing
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_learning_signals() {
|
||||
let engine = Arc::new(SonaEngine::new(128));
|
||||
let num_threads = 6;
|
||||
let signals_per_thread = 30;
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for thread_id in 0..num_threads {
|
||||
let engine_clone = Arc::clone(&engine);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
for i in 0..signals_per_thread {
|
||||
let quality = 0.7 + (((thread_id + i) % 10) as f32) * 0.02;
|
||||
let embedding = vec![0.3 + (thread_id as f32 * 0.1); 128];
|
||||
|
||||
let mut builder = engine_clone.begin_trajectory(embedding);
|
||||
builder.add_step(vec![0.5; 128], vec![], quality - 0.1);
|
||||
builder.add_step(vec![0.6; 128], vec![], quality);
|
||||
builder.add_step(vec![0.7; 128], vec![], quality + 0.05);
|
||||
|
||||
engine_clone.end_trajectory(builder, quality);
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
for handle in handles {
|
||||
handle
|
||||
.join()
|
||||
.expect("Thread panicked during signal processing");
|
||||
}
|
||||
|
||||
// Verify learning occurred
|
||||
engine.flush();
|
||||
let stats = engine.stats();
|
||||
assert!(stats.trajectories_buffered > 0 || stats.trajectories_dropped > 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 9: Instant Loop Latency Performance
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_instant_loop_latency() {
|
||||
let engine = SonaEngine::new(256);
|
||||
let iterations = 100;
|
||||
let mut latencies = Vec::with_capacity(iterations);
|
||||
|
||||
for _i in 0..iterations {
|
||||
let start = Instant::now();
|
||||
|
||||
// Record trajectory
|
||||
let mut builder = engine.begin_trajectory(vec![0.5; 256]);
|
||||
builder.add_step(vec![0.6; 256], vec![], 0.8);
|
||||
builder.add_step(vec![0.7; 256], vec![], 0.85);
|
||||
engine.end_trajectory(builder, 0.83);
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
latencies.push(elapsed);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
let total_micros: u128 = latencies.iter().map(|d| d.as_micros()).sum();
|
||||
let avg_micros = total_micros / iterations as u128;
|
||||
let max_latency = latencies.iter().max().unwrap();
|
||||
|
||||
println!("Instant loop latency:");
|
||||
println!(" Average: {}μs", avg_micros);
|
||||
println!(" Max: {}μs", max_latency.as_micros());
|
||||
|
||||
// Verify instant loop completes in <1ms on average
|
||||
assert!(
|
||||
avg_micros < 1000,
|
||||
"Average instant loop latency {}μs exceeds 1ms threshold",
|
||||
avg_micros
|
||||
);
|
||||
|
||||
// Verify no individual recording exceeds 5ms (generous bound)
|
||||
assert!(
|
||||
max_latency.as_millis() < 5,
|
||||
"Max latency {}ms exceeds acceptable bound",
|
||||
max_latency.as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 10: Lock-Free Trajectory Recording Performance
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_lockfree_trajectory_buffer() {
|
||||
let buffer = TrajectoryBuffer::new(1000);
|
||||
let iterations = 500;
|
||||
|
||||
let mut record_times = Vec::with_capacity(iterations);
|
||||
|
||||
for i in 0..iterations {
|
||||
let mut trajectory = QueryTrajectory::new(i as u64, vec![0.5; 64]);
|
||||
trajectory.add_step(TrajectoryStep::new(vec![0.6; 64], vec![], 0.8, 0));
|
||||
trajectory.finalize(0.82, 1000);
|
||||
|
||||
let start = Instant::now();
|
||||
let recorded = buffer.record(trajectory);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
if recorded {
|
||||
record_times.push(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify non-blocking behavior
|
||||
let avg_nanos: u128 =
|
||||
record_times.iter().map(|d| d.as_nanos()).sum::<u128>() / record_times.len() as u128;
|
||||
|
||||
println!("Lock-free buffer record:");
|
||||
println!(" Average: {}ns", avg_nanos);
|
||||
println!(" Total recorded: {}/{}", record_times.len(), iterations);
|
||||
|
||||
// Lock-free operations should be extremely fast (sub-microsecond)
|
||||
assert!(
|
||||
avg_nanos < 10_000,
|
||||
"Average record time {}ns suggests blocking behavior",
|
||||
avg_nanos
|
||||
);
|
||||
|
||||
// Verify high success rate
|
||||
let success_rate = buffer.success_rate();
|
||||
assert!(
|
||||
success_rate > 0.9,
|
||||
"Success rate {} is too low, expected >90%",
|
||||
success_rate
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 11: Background Loop Pattern Extraction
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_background_loop_pattern_extraction() {
|
||||
let engine = SonaEngine::new(256);
|
||||
|
||||
// Generate diverse trajectories
|
||||
for cluster in 0..5 {
|
||||
for i in 0..30 {
|
||||
let mut embedding = vec![0.0; 256];
|
||||
|
||||
// Create cluster-specific patterns
|
||||
let start_idx = cluster * 50;
|
||||
for j in start_idx..(start_idx + 50) {
|
||||
embedding[j] = 0.7 + (i as f32 * 0.01);
|
||||
}
|
||||
|
||||
let mut builder = engine.begin_trajectory(embedding);
|
||||
builder.add_step(vec![0.5; 256], vec![], 0.8);
|
||||
builder.add_step(vec![0.6; 256], vec![], 0.85);
|
||||
engine.end_trajectory(builder, 0.82);
|
||||
}
|
||||
}
|
||||
|
||||
// Force background learning
|
||||
let result = engine.force_learn();
|
||||
let stats = engine.stats();
|
||||
|
||||
// Pattern extraction depends on quality threshold and minimum cluster size
|
||||
// With quality_threshold=0.7 (default), patterns with avg_quality < 0.7 are filtered
|
||||
println!(
|
||||
"Patterns stored: {} from 150 trajectories",
|
||||
stats.patterns_stored
|
||||
);
|
||||
|
||||
// Just verify the learning cycle ran successfully
|
||||
assert!(
|
||||
result.contains("Forced learning:"),
|
||||
"Background learning should complete"
|
||||
);
|
||||
assert!(
|
||||
result.contains("150 trajectories"),
|
||||
"Expected 150 trajectories processed"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 12: EWC++ Multi-Task Memory
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_ewc_multitask_memory() {
|
||||
let config = EwcConfig {
|
||||
param_count: 128,
|
||||
max_tasks: 5,
|
||||
initial_lambda: 500.0,
|
||||
boundary_threshold: 1.5,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut ewc = EwcPlusPlus::new(config);
|
||||
|
||||
// Simulate multiple tasks with gradient updates
|
||||
for task_id in 0..4 {
|
||||
// Each task has distinct gradient pattern
|
||||
let gradient_base = 0.2 * task_id as f32;
|
||||
|
||||
for _ in 0..50 {
|
||||
let gradients: Vec<f32> = (0..128)
|
||||
.map(|i| gradient_base + (i as f32 * 0.001))
|
||||
.collect();
|
||||
ewc.update_fisher(&gradients);
|
||||
}
|
||||
|
||||
// Start new task to save Fisher information
|
||||
ewc.start_new_task();
|
||||
}
|
||||
|
||||
// Verify tasks were recorded
|
||||
assert_eq!(ewc.task_count(), 4, "Expected 4 tasks in memory");
|
||||
assert_eq!(ewc.current_task_id(), 4, "Expected current task ID to be 4");
|
||||
|
||||
// Test gradient constraint application
|
||||
let test_gradients = vec![1.0; 128];
|
||||
let constrained = ewc.apply_constraints(&test_gradients);
|
||||
|
||||
// Constrained gradients should be smaller (protected by Fisher)
|
||||
let original_norm: f32 = test_gradients.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
let constrained_norm: f32 = constrained.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
|
||||
assert!(
|
||||
constrained_norm <= original_norm,
|
||||
"EWC constraints should reduce gradient magnitude"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 13: Complete Integration - End-to-End
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_complete_integration_workflow() {
|
||||
// Build engine with full configuration
|
||||
let engine = SonaEngineBuilder::new()
|
||||
.hidden_dim(256)
|
||||
.micro_lora_rank(2)
|
||||
.base_lora_rank(16)
|
||||
.micro_lr(0.002)
|
||||
.base_lr(0.0002)
|
||||
.ewc_lambda(800.0)
|
||||
.pattern_clusters(20)
|
||||
.buffer_capacity(2000)
|
||||
.quality_threshold(0.6)
|
||||
.build();
|
||||
|
||||
// Phase 1: Initial learning (100 trajectories)
|
||||
for i in 0..100 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.3 + (i as f32 * 0.001); 256]);
|
||||
builder.add_step(vec![0.4; 256], vec![], 0.75);
|
||||
builder.add_step(vec![0.5; 256], vec![], 0.8);
|
||||
builder.add_step(vec![0.6; 256], vec![], 0.85);
|
||||
engine.end_trajectory(builder, 0.78);
|
||||
}
|
||||
|
||||
engine.flush();
|
||||
let stats1 = engine.stats();
|
||||
assert_eq!(stats1.trajectories_buffered, 100);
|
||||
|
||||
// Phase 2: Background learning
|
||||
let result1 = engine.force_learn();
|
||||
let stats2 = engine.stats();
|
||||
assert!(stats2.patterns_stored > 0);
|
||||
|
||||
// Phase 3: Apply learning (inference simulation)
|
||||
let query = vec![0.35; 256];
|
||||
let patterns = engine.find_patterns(&query, 5);
|
||||
assert!(!patterns.is_empty());
|
||||
|
||||
// Phase 4: More learning with different distribution
|
||||
for i in 0..100 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.7 + (i as f32 * 0.001); 256]);
|
||||
builder.add_step(vec![0.75; 256], vec![], 0.85);
|
||||
builder.add_step(vec![0.8; 256], vec![], 0.88);
|
||||
builder.add_step(vec![0.85; 256], vec![], 0.9);
|
||||
engine.end_trajectory(builder, 0.87);
|
||||
}
|
||||
|
||||
// Phase 5: Second background learning (task boundary detection)
|
||||
let result2 = engine.force_learn();
|
||||
let stats3 = engine.stats();
|
||||
|
||||
// Patterns should have increased
|
||||
assert!(stats3.patterns_stored >= stats2.patterns_stored);
|
||||
|
||||
// Phase 6: Apply both LoRA tiers
|
||||
let input = vec![1.0; 256];
|
||||
let mut micro_output = vec![0.0; 256];
|
||||
let mut base_output = vec![0.0; 256];
|
||||
|
||||
engine.apply_micro_lora(&input, &mut micro_output);
|
||||
engine.apply_base_lora(0, &input, &mut base_output);
|
||||
|
||||
// Both should produce output after learning
|
||||
let micro_sum: f32 = micro_output.iter().map(|&x: &f32| x.abs()).sum();
|
||||
let base_sum: f32 = base_output.iter().map(|&x: &f32| x.abs()).sum();
|
||||
|
||||
assert!(micro_sum > 0.0, "Micro-LoRA should be active");
|
||||
// Base LoRA might be zero initially depending on implementation
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 14: Pattern Quality Filtering
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_pattern_quality_filtering() {
|
||||
let engine = SonaEngineBuilder::new()
|
||||
.hidden_dim(128)
|
||||
.quality_threshold(0.7)
|
||||
.pattern_clusters(10)
|
||||
.build();
|
||||
|
||||
// Add high-quality trajectories
|
||||
for i in 0..50 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.8; 128]);
|
||||
builder.add_step(vec![0.85; 128], vec![], 0.9);
|
||||
engine.end_trajectory(builder, 0.85);
|
||||
}
|
||||
|
||||
// Add low-quality trajectories (should be filtered)
|
||||
for i in 0..50 {
|
||||
let mut builder = engine.begin_trajectory(vec![0.2; 128]);
|
||||
builder.add_step(vec![0.25; 128], vec![], 0.3);
|
||||
engine.end_trajectory(builder, 0.28);
|
||||
}
|
||||
|
||||
let result = engine.force_learn();
|
||||
let stats = engine.stats();
|
||||
|
||||
// Only high-quality patterns should be stored
|
||||
let patterns = engine.find_patterns(&vec![0.8; 128], 10);
|
||||
|
||||
// Verify patterns have quality above threshold
|
||||
for pattern in &patterns {
|
||||
assert!(
|
||||
pattern.avg_quality >= 0.7,
|
||||
"Pattern quality {} below threshold",
|
||||
pattern.avg_quality
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 15: Engine Enable/Disable
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_engine_enable_disable() {
|
||||
let mut engine = SonaEngine::new(64);
|
||||
|
||||
assert!(engine.is_enabled());
|
||||
|
||||
// Record with enabled engine
|
||||
let mut builder = engine.begin_trajectory(vec![0.5; 64]);
|
||||
builder.add_step(vec![0.6; 64], vec![], 0.8);
|
||||
engine.end_trajectory(builder, 0.82);
|
||||
|
||||
let stats1 = engine.stats();
|
||||
assert_eq!(stats1.trajectories_buffered, 1);
|
||||
|
||||
// Disable engine
|
||||
engine.set_enabled(false);
|
||||
assert!(!engine.is_enabled());
|
||||
|
||||
// Record with disabled engine (should be ignored)
|
||||
let mut builder = engine.begin_trajectory(vec![0.5; 64]);
|
||||
builder.add_step(vec![0.6; 64], vec![], 0.8);
|
||||
engine.end_trajectory(builder, 0.82);
|
||||
|
||||
let stats2 = engine.stats();
|
||||
assert_eq!(
|
||||
stats2.trajectories_buffered, 1,
|
||||
"Disabled engine should not record"
|
||||
);
|
||||
|
||||
// Re-enable
|
||||
engine.set_enabled(true);
|
||||
let mut builder = engine.begin_trajectory(vec![0.5; 64]);
|
||||
builder.add_step(vec![0.6; 64], vec![], 0.8);
|
||||
engine.end_trajectory(builder, 0.82);
|
||||
|
||||
let stats3 = engine.stats();
|
||||
assert_eq!(stats3.trajectories_buffered, 2);
|
||||
}
|
||||
Reference in New Issue
Block a user