stemedb/crates/stemedb-ingest/src/worker/tests/validation_boundaries.rs
jordan 42d4e09508 feat: Index persistence (Phase 5C) - vector hot/cold, visual checkpoint
Phase 5C (Index Persistence) implementation:
- PersistentVectorIndex with hot/cold architecture
  - Hot: in-memory HNSW for recent vectors
  - Cold: memory-mapped HNSW loaded from disk
  - Background builder for WAL replay and atomic swap
  - BLAKE3 integrity verification
- PersistentVisualIndex with checkpoint persistence
  - BkTreeSnapshot with rkyv serialization
  - CRC32C corruption detection
  - Atomic write pattern (temp → fsync → rename)
- Key codec additions for vector index metadata
- Split large files into modules (<500 lines each)
  - battery_pre_sentinel.rs → battery/ directory
  - visual_index.rs → visual_index/ directory
  - persistent.rs → persistent/ directory
- Refactored ingest worker tests for clarity
- Updated roadmap to mark Phase 5 complete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 15:43:18 -07:00

356 lines
12 KiB
Rust

//! Boundary and edge-case validation tests.
//!
//! Tests for infinite confidence/weight, future timestamps,
//! and boundary values (0.0, 1.0) for confidence.
use super::*;
use crate::error::IngestError;
/// Test: Assertions with infinite confidence are rejected.
#[tokio::test]
async fn test_rejects_infinite_confidence() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = "Test:inf_confidence";
let signature = signing_key.sign(message.as_bytes());
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "inf_confidence".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp: 1000,
}],
confidence: f32::INFINITY,
timestamp: 1000,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_err(), "Should reject infinite confidence");
let err = result.unwrap_err();
assert!(
matches!(err, IngestError::InputValidation(_)),
"Error should be InputValidation, got: {:?}",
err
);
assert!(err.to_string().contains("infinite"));
}
/// Test: Votes with NaN weight are rejected.
#[tokio::test]
async fn test_rejects_nan_vote_weight() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let vote = Vote {
assertion_hash: [1u8; 32],
agent_id: [2u8; 32],
weight: f32::NAN,
signature: [3u8; 64],
timestamp: 1000,
source_url: None,
observed_context: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_vote(&vote).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_err(), "Should reject NaN vote weight");
let err = result.unwrap_err();
assert!(
matches!(err, IngestError::InputValidation(_)),
"Error should be InputValidation, got: {:?}",
err
);
assert!(err.to_string().contains("NaN"));
}
/// Test: Votes with infinite weight are rejected.
#[tokio::test]
async fn test_rejects_infinite_vote_weight() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let vote = Vote {
assertion_hash: [1u8; 32],
agent_id: [2u8; 32],
weight: f32::INFINITY,
signature: [3u8; 64],
timestamp: 1000,
source_url: None,
observed_context: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_vote(&vote).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_err(), "Should reject infinite vote weight");
let err = result.unwrap_err();
assert!(
matches!(err, IngestError::InputValidation(_)),
"Error should be InputValidation, got: {:?}",
err
);
assert!(err.to_string().contains("infinite"));
}
/// Test: Assertions with timestamp far in the future are rejected.
#[tokio::test]
async fn test_rejects_future_timestamp() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = "Test:future";
let signature = signing_key.sign(message.as_bytes());
// Create timestamp 2 hours in the future (should be rejected)
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let future_timestamp = now + 7200; // 2 hours ahead
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "future".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp: 1000,
}],
confidence: 0.9,
timestamp: future_timestamp,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_err(), "Should reject timestamp more than 1 hour in future");
let err = result.unwrap_err();
assert!(
matches!(err, IngestError::InputValidation(_)),
"Error should be InputValidation, got: {:?}",
err
);
assert!(err.to_string().contains("timestamp"));
}
/// Test: Assertions with timestamp slightly in the future (< 1 hour) are accepted.
#[tokio::test]
async fn test_accepts_near_future_timestamp() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = "Test:near_future";
let signature = signing_key.sign(message.as_bytes());
// Create timestamp 30 minutes in the future (should be accepted)
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let near_future_timestamp = now + 1800; // 30 minutes ahead
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "near_future".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp: 1000,
}],
confidence: 0.9,
timestamp: near_future_timestamp,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_ok(), "Should accept timestamp within 1 hour clock skew");
}
/// Test: Edge case - confidence exactly 0.0 is accepted.
#[tokio::test]
async fn test_accepts_zero_confidence() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = "Test:zero_conf";
let signature = signing_key.sign(message.as_bytes());
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "zero_conf".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp: 1000,
}],
confidence: 0.0, // Valid: boundary case
timestamp: 1000,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_ok(), "Should accept confidence = 0.0");
}
/// Test: Edge case - confidence exactly 1.0 is accepted.
#[tokio::test]
async fn test_accepts_one_confidence() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = "Test:one_conf";
let signature = signing_key.sign(message.as_bytes());
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "one_conf".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp: 1000,
}],
confidence: 1.0, // Valid: boundary case
timestamp: 1000,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_ok(), "Should accept confidence = 1.0");
}