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>
356 lines
12 KiB
Rust
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");
|
|
}
|