//! Shared test helpers for the Episteme workspace. //! //! Provides [`AssertionBuilder`] and factory functions to eliminate duplicate //! test assertion construction across crates. Import via: //! //! ```rust,ignore //! use stemedb_core::testing::{AssertionBuilder, test_vote, test_epoch}; //! ``` use crate::types::{ Assertion, Epoch, HlcTimestamp, LifecycleStage, ObjectValue, SignatureEntry, SourceClass, SupersessionType, Vote, }; /// Builder for constructing test [`Assertion`] instances. /// /// Every field has a sensible default so callers only override what they need. /// /// # Examples /// /// ```rust,ignore /// // Minimal /// let a = AssertionBuilder::new().build(); /// /// // Override subject + timestamp /// let a = AssertionBuilder::new() /// .subject("Tesla") /// .timestamp(2000) /// .build(); /// /// // Full control /// let a = AssertionBuilder::new() /// .subject("Tesla") /// .predicate("revenue") /// .object_number(96.7) /// .confidence(0.8) /// .lifecycle(LifecycleStage::Proposed) /// .agent_id([5u8; 32]) /// .timestamp(3000) /// .build(); /// ``` pub struct AssertionBuilder { subject: String, predicate: String, object: ObjectValue, parent_hash: Option<[u8; 32]>, source_hash: [u8; 32], source_class: SourceClass, visual_hash: Option<[u8; 8]>, epoch: Option<[u8; 32]>, source_metadata: Option>, lifecycle: LifecycleStage, signatures: Option>, agent_id: [u8; 32], confidence: f32, timestamp: u64, hlc_timestamp: HlcTimestamp, vector: Option>, } impl Default for AssertionBuilder { fn default() -> Self { Self::new() } } impl AssertionBuilder { /// Create a new builder with sensible test defaults. pub fn new() -> Self { Self { subject: "test_subject".to_string(), predicate: "test_predicate".to_string(), object: ObjectValue::Number(100.0), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, // Default to middle tier for tests visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Approved, signatures: None, // Will use agent_id to build default agent_id: [1u8; 32], confidence: 0.9, timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, } } /// Set the subject. pub fn subject(mut self, subject: &str) -> Self { self.subject = subject.to_string(); self } /// Set the predicate. pub fn predicate(mut self, predicate: &str) -> Self { self.predicate = predicate.to_string(); self } /// Set the object to a numeric value. pub fn object_number(mut self, value: f64) -> Self { self.object = ObjectValue::Number(value); self } /// Set the object to a text value. pub fn object_text(mut self, value: &str) -> Self { self.object = ObjectValue::Text(value.to_string()); self } /// Set the object to an arbitrary ObjectValue. pub fn object(mut self, value: ObjectValue) -> Self { self.object = value; self } /// Set the confidence (0.0 to 1.0). pub fn confidence(mut self, confidence: f32) -> Self { self.confidence = confidence; self } /// Set the timestamp. pub fn timestamp(mut self, timestamp: u64) -> Self { self.timestamp = timestamp; self } /// Set the HLC timestamp for distributed causal ordering. /// /// This provides total ordering even with clock skew between nodes. /// Most tests can rely on the default (HlcTimestamp::default()). pub fn hlc_timestamp(mut self, hlc_timestamp: HlcTimestamp) -> Self { self.hlc_timestamp = hlc_timestamp; self } /// Set the lifecycle stage. pub fn lifecycle(mut self, lifecycle: LifecycleStage) -> Self { self.lifecycle = lifecycle; self } /// Set the agent_id used in the default signature. pub fn agent_id(mut self, agent_id: [u8; 32]) -> Self { self.agent_id = agent_id; self } /// Set the source hash. pub fn source_hash(mut self, hash: [u8; 32]) -> Self { self.source_hash = hash; self } /// Set the source class (authority tier). pub fn source_class(mut self, source_class: SourceClass) -> Self { self.source_class = source_class; self } /// Set the parent hash. pub fn parent_hash(mut self, hash: [u8; 32]) -> Self { self.parent_hash = Some(hash); self } /// Set the visual hash. pub fn visual_hash(mut self, hash: [u8; 8]) -> Self { self.visual_hash = Some(hash); self } /// Set the epoch. pub fn epoch(mut self, epoch: [u8; 32]) -> Self { self.epoch = Some(epoch); self } /// Set the vector embedding. pub fn vector(mut self, vector: Vec) -> Self { self.vector = Some(vector); self } /// Set the source metadata from a JSON string. /// Stores as bytes for rkyv compatibility. pub fn source_metadata_json(mut self, json: &str) -> Self { self.source_metadata = Some(json.as_bytes().to_vec()); self } /// Set the source metadata as raw bytes. pub fn source_metadata(mut self, metadata: Vec) -> Self { self.source_metadata = Some(metadata); self } /// Provide explicit signatures (overrides the default single-signature behavior). pub fn signatures(mut self, signatures: Vec) -> Self { self.signatures = Some(signatures); self } /// Build the [`Assertion`]. pub fn build(self) -> Assertion { let signatures = self.signatures.unwrap_or_else(|| { vec![SignatureEntry { agent_id: self.agent_id, signature: [2u8; 64], timestamp: self.timestamp, version: 1, // Default to v1 for backward compatibility }] }); Assertion { subject: self.subject, predicate: self.predicate, object: self.object, parent_hash: self.parent_hash, source_hash: self.source_hash, source_class: self.source_class, visual_hash: self.visual_hash, epoch: self.epoch, source_metadata: self.source_metadata, lifecycle: self.lifecycle, signatures, confidence: self.confidence, timestamp: self.timestamp, hlc_timestamp: self.hlc_timestamp, vector: self.vector, } } } /// Create a test [`Vote`] with the given parameters. pub fn test_vote( assertion_hash: [u8; 32], agent_id: [u8; 32], weight: f32, timestamp: u64, ) -> Vote { Vote { assertion_hash, agent_id, weight, signature: [0u8; 64], timestamp, source_url: None, observed_context: None, } } /// Create a test [`Epoch`] with defaults. pub fn test_epoch() -> Epoch { Epoch { id: [4u8; 32], name: "Test Epoch".to_string(), supersedes: None, supersession_type: None, start_timestamp: 1000, end_timestamp: None, } } /// Create a test [`Epoch`] with a supersession relationship. pub fn test_epoch_with_supersession( id: [u8; 32], name: &str, supersedes: [u8; 32], supersession_type: SupersessionType, ) -> Epoch { Epoch { id, name: name.to_string(), supersedes: Some(supersedes), supersession_type: Some(supersession_type), start_timestamp: 1000, end_timestamp: None, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_builder_defaults() { let a = AssertionBuilder::new().build(); assert_eq!(a.subject, "test_subject"); assert_eq!(a.predicate, "test_predicate"); assert_eq!(a.lifecycle, LifecycleStage::Approved); assert!((a.confidence - 0.9).abs() < f32::EPSILON); assert_eq!(a.timestamp, 1000); assert_eq!(a.signatures.len(), 1); assert_eq!(a.signatures[0].agent_id, [1u8; 32]); } #[test] fn test_builder_overrides() { let a = AssertionBuilder::new() .subject("Tesla") .predicate("revenue") .object_number(96.7) .confidence(0.85) .timestamp(5000) .lifecycle(LifecycleStage::Proposed) .agent_id([5u8; 32]) .build(); assert_eq!(a.subject, "Tesla"); assert_eq!(a.predicate, "revenue"); assert_eq!(a.object, ObjectValue::Number(96.7)); assert!((a.confidence - 0.85).abs() < f32::EPSILON); assert_eq!(a.timestamp, 5000); assert_eq!(a.lifecycle, LifecycleStage::Proposed); assert_eq!(a.signatures[0].agent_id, [5u8; 32]); } #[test] fn test_builder_custom_signatures() { let sigs = vec![ SignatureEntry { agent_id: [10u8; 32], signature: [11u8; 64], timestamp: 100, version: 1, }, SignatureEntry { agent_id: [20u8; 32], signature: [21u8; 64], timestamp: 200, version: 1, }, ]; let a = AssertionBuilder::new().signatures(sigs).build(); assert_eq!(a.signatures.len(), 2); assert_eq!(a.signatures[0].agent_id, [10u8; 32]); } #[test] fn test_builder_text_object() { let a = AssertionBuilder::new().object_text("hello").build(); assert_eq!(a.object, ObjectValue::Text("hello".to_string())); } #[test] fn test_vote_factory() { let v = test_vote([1u8; 32], [2u8; 32], 0.8, 3000); assert_eq!(v.assertion_hash, [1u8; 32]); assert_eq!(v.agent_id, [2u8; 32]); assert!((v.weight - 0.8).abs() < f32::EPSILON); assert_eq!(v.timestamp, 3000); } #[test] fn test_epoch_factory() { let e = test_epoch(); assert_eq!(e.id, [4u8; 32]); assert_eq!(e.name, "Test Epoch"); assert!(e.supersedes.is_none()); } }