phytrace_sdk/models/
event.rs

1//! UDM Event - The core event envelope that contains all domain data.
2//!
3//! The `UdmEvent` is the primary data structure emitted by PhyTrace SDK.
4//! It contains optional fields for all 23 UDM domains plus metadata for
5//! event classification, provenance, and extensions.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use validator::Validate;
11
12use crate::models::domains::{
13    ActuatorsDomain, AiDomain, AudioDomain, CommunicationDomain, ComplianceDomain, ComputeDomain,
14    ContextDomain, CoordinationDomain, EnvironmentInteractionDomain, HriDomain, IdentityDomain,
15    LocationDomain, MaintenanceDomain, ManipulationDomain, MotionDomain, NavigationDomain,
16    OperationalDomain, PayloadDomain, PerceptionDomain, PowerDomain, SafetyDomain,
17    SimulationDomain, ThermalDomain,
18};
19use crate::models::enums::{EventType, SourceType};
20
21/// UDM Event - The unified data model event envelope.
22///
23/// This is the primary structure emitted by the PhyTrace SDK. It contains:
24/// - Required envelope fields (event_id, timestamp, source_type)
25/// - Optional domain data for all 23 UDM domains
26/// - Provenance information for data integrity verification
27/// - Extension point for custom data
28///
29/// # Example
30///
31/// ```rust
32/// use phytrace_sdk::{UdmEvent, EventType, SourceType};
33/// use phytrace_sdk::models::domains::{IdentityDomain, LocationDomain};
34/// use phytrace_sdk::models::domains::location::LocalCoordinates;
35///
36/// let event = UdmEvent::new(SourceType::Amr)
37///     .with_event_type(EventType::TelemetryPeriodic)
38///     .with_identity(IdentityDomain {
39///         source_id: Some("robot-001".to_string()),
40///         ..Default::default()
41///     })
42///     .with_location(LocationDomain {
43///         local: Some(LocalCoordinates {
44///             x_m: Some(10.0),
45///             y_m: Some(20.0),
46///             ..Default::default()
47///         }),
48///         ..Default::default()
49///     });
50/// ```
51#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
52pub struct UdmEvent {
53    // =========================================================================
54    // Required Envelope Fields
55    // =========================================================================
56    /// Unique event identifier (UUIDv7 for time-ordering).
57    pub event_id: String,
58
59    /// When data was captured at source (ISO 8601 format).
60    /// Note: This matches PhyCloud's expected field name.
61    #[serde(alias = "timestamp")]
62    pub captured_at: DateTime<Utc>,
63
64    /// Type of event.
65    #[serde(default)]
66    pub event_type: EventType,
67
68    /// Unique source identifier (e.g., robot-001).
69    /// This is a required top-level field for PhyCloud compatibility.
70    #[serde(default)]
71    pub source_id: String,
72
73    /// Type of source generating this event.
74    pub source_type: SourceType,
75
76    /// UDM schema version.
77    #[serde(alias = "schema_version")]
78    pub udm_version: String,
79
80    /// SDK version that generated this event.
81    pub sdk_version: String,
82
83    // =========================================================================
84    // UDM Domains (all optional)
85    // =========================================================================
86    /// Identity domain - who/what is reporting.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub identity: Option<IdentityDomain>,
89
90    /// Location domain - where the source is.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub location: Option<LocationDomain>,
93
94    /// Motion domain - velocity, acceleration, odometry.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub motion: Option<MotionDomain>,
97
98    /// Power domain - battery, charging, energy.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub power: Option<PowerDomain>,
101
102    /// Operational domain - mode, state, tasks.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub operational: Option<OperationalDomain>,
105
106    /// Navigation domain - localization, path, obstacles.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub navigation: Option<NavigationDomain>,
109
110    /// Perception domain - sensors, detections.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub perception: Option<PerceptionDomain>,
113
114    /// Safety domain - e-stop, zones, violations.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub safety: Option<SafetyDomain>,
117
118    /// Actuators domain - motors, joints, grippers.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub actuators: Option<ActuatorsDomain>,
121
122    /// Communication domain - network, fleet comms.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub communication: Option<CommunicationDomain>,
125
126    /// Compute domain - CPU, memory, processes.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub compute: Option<ComputeDomain>,
129
130    /// AI domain - models, decisions, anomalies.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub ai: Option<AiDomain>,
133
134    /// Maintenance domain - component health, diagnostics.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub maintenance: Option<MaintenanceDomain>,
137
138    /// Context domain - time, facility, weather context.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub context: Option<ContextDomain>,
141
142    /// Payload domain - carried items, compartments.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub payload: Option<PayloadDomain>,
145
146    /// Manipulation domain - arms, grippers, workspace.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub manipulation: Option<ManipulationDomain>,
149
150    /// HRI domain - human interaction, social navigation.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub hri: Option<HriDomain>,
153
154    /// Coordination domain - fleet coordination, formations.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub coordination: Option<CoordinationDomain>,
157
158    /// Simulation domain - simulator info, scenarios.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub simulation: Option<SimulationDomain>,
161
162    /// Thermal domain - temperatures, cooling.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub thermal: Option<ThermalDomain>,
165
166    /// Audio domain - microphones, speakers, sounds.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub audio: Option<AudioDomain>,
169
170    /// Environment interaction domain - doors, elevators.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub environment_interaction: Option<EnvironmentInteractionDomain>,
173
174    /// Compliance domain - certifications, safety standards.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub compliance: Option<ComplianceDomain>,
177
178    // =========================================================================
179    // Provenance & Extensions
180    // =========================================================================
181    /// Provenance information for data integrity.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub provenance: Option<Provenance>,
184
185    /// Extension point for custom data.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub extensions: Option<serde_json::Value>,
188}
189
190/// Provenance information for data integrity verification.
191#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192pub struct Provenance {
193    /// HMAC-SHA256 signature of the event.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub signature: Option<String>,
196
197    /// Key ID used for signing.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub key_id: Option<String>,
200
201    /// Signature algorithm (e.g., "hmac-sha256").
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub algorithm: Option<String>,
204
205    /// Fields included in signature.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub signed_fields: Option<Vec<String>>,
208
209    /// Timestamp when signature was created.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub signed_at: Option<DateTime<Utc>>,
212}
213
214impl UdmEvent {
215    /// Create a new UDM event with required fields.
216    ///
217    /// Automatically generates a UUIDv7 event ID and sets the captured_at timestamp.
218    pub fn new(source_type: SourceType) -> Self {
219        Self {
220            event_id: Uuid::now_v7().to_string(),
221            captured_at: Utc::now(),
222            event_type: EventType::default(),
223            source_type,
224            udm_version: crate::UDM_VERSION.to_string(),
225            sdk_version: crate::SDK_VERSION.to_string(),
226            ..Default::default()
227        }
228    }
229
230    /// Set the event type.
231    pub fn with_event_type(mut self, event_type: EventType) -> Self {
232        self.event_type = event_type;
233        self
234    }
235
236    /// Set the identity domain.
237    pub fn with_identity(mut self, identity: IdentityDomain) -> Self {
238        self.identity = Some(identity);
239        self
240    }
241
242    /// Set the location domain.
243    pub fn with_location(mut self, location: LocationDomain) -> Self {
244        self.location = Some(location);
245        self
246    }
247
248    /// Set the motion domain.
249    pub fn with_motion(mut self, motion: MotionDomain) -> Self {
250        self.motion = Some(motion);
251        self
252    }
253
254    /// Set the power domain.
255    pub fn with_power(mut self, power: PowerDomain) -> Self {
256        self.power = Some(power);
257        self
258    }
259
260    /// Set the operational domain.
261    pub fn with_operational(mut self, operational: OperationalDomain) -> Self {
262        self.operational = Some(operational);
263        self
264    }
265
266    /// Set the navigation domain.
267    pub fn with_navigation(mut self, navigation: NavigationDomain) -> Self {
268        self.navigation = Some(navigation);
269        self
270    }
271
272    /// Set the perception domain.
273    pub fn with_perception(mut self, perception: PerceptionDomain) -> Self {
274        self.perception = Some(perception);
275        self
276    }
277
278    /// Set the safety domain.
279    pub fn with_safety(mut self, safety: SafetyDomain) -> Self {
280        self.safety = Some(safety);
281        self
282    }
283
284    /// Set the actuators domain.
285    pub fn with_actuators(mut self, actuators: ActuatorsDomain) -> Self {
286        self.actuators = Some(actuators);
287        self
288    }
289
290    /// Set the communication domain.
291    pub fn with_communication(mut self, communication: CommunicationDomain) -> Self {
292        self.communication = Some(communication);
293        self
294    }
295
296    /// Set the compute domain.
297    pub fn with_compute(mut self, compute: ComputeDomain) -> Self {
298        self.compute = Some(compute);
299        self
300    }
301
302    /// Set the AI domain.
303    pub fn with_ai(mut self, ai: AiDomain) -> Self {
304        self.ai = Some(ai);
305        self
306    }
307
308    /// Set the maintenance domain.
309    pub fn with_maintenance(mut self, maintenance: MaintenanceDomain) -> Self {
310        self.maintenance = Some(maintenance);
311        self
312    }
313
314    /// Set the context domain.
315    pub fn with_context(mut self, context: ContextDomain) -> Self {
316        self.context = Some(context);
317        self
318    }
319
320    /// Set the payload domain.
321    pub fn with_payload(mut self, payload: PayloadDomain) -> Self {
322        self.payload = Some(payload);
323        self
324    }
325
326    /// Set the manipulation domain.
327    pub fn with_manipulation(mut self, manipulation: ManipulationDomain) -> Self {
328        self.manipulation = Some(manipulation);
329        self
330    }
331
332    /// Set the HRI domain.
333    pub fn with_hri(mut self, hri: HriDomain) -> Self {
334        self.hri = Some(hri);
335        self
336    }
337
338    /// Set the coordination domain.
339    pub fn with_coordination(mut self, coordination: CoordinationDomain) -> Self {
340        self.coordination = Some(coordination);
341        self
342    }
343
344    /// Set the simulation domain.
345    pub fn with_simulation(mut self, simulation: SimulationDomain) -> Self {
346        self.simulation = Some(simulation);
347        self
348    }
349
350    /// Set the thermal domain.
351    pub fn with_thermal(mut self, thermal: ThermalDomain) -> Self {
352        self.thermal = Some(thermal);
353        self
354    }
355
356    /// Set the audio domain.
357    pub fn with_audio(mut self, audio: AudioDomain) -> Self {
358        self.audio = Some(audio);
359        self
360    }
361
362    /// Set the environment interaction domain.
363    pub fn with_environment_interaction(
364        mut self,
365        environment_interaction: EnvironmentInteractionDomain,
366    ) -> Self {
367        self.environment_interaction = Some(environment_interaction);
368        self
369    }
370
371    /// Set the compliance domain.
372    pub fn with_compliance(mut self, compliance: ComplianceDomain) -> Self {
373        self.compliance = Some(compliance);
374        self
375    }
376
377    /// Set custom extensions.
378    pub fn with_extensions(mut self, extensions: serde_json::Value) -> Self {
379        self.extensions = Some(extensions);
380        self
381    }
382
383    /// Inject license metadata into the event's extensions.
384    ///
385    /// This is called automatically by the agent before sending events.
386    pub fn inject_license_metadata(&mut self, metadata: &crate::core::license::LicenseMetadata) {
387        let license_data = serde_json::json!({
388            "_license_status": metadata.status,
389            "_license_tenant_id": metadata.tenant_id,
390            "_license_plan": metadata.plan,
391            "_quarantine": metadata.quarantine,
392            "_grace_period_start": metadata.grace_started_at,
393            "_grace_period_expires": metadata.grace_expires_at,
394            "_last_online_check": metadata.last_online_check,
395        });
396
397        if let Some(ref mut extensions) = self.extensions {
398            if let Some(obj) = extensions.as_object_mut() {
399                for (key, value) in license_data.as_object().unwrap() {
400                    obj.insert(key.clone(), value.clone());
401                }
402            }
403        } else {
404            self.extensions = Some(license_data);
405        }
406    }
407
408    /// Set provenance information.
409    pub fn with_provenance(mut self, provenance: Provenance) -> Self {
410        self.provenance = Some(provenance);
411        self
412    }
413
414    /// Check if the event has any domain data.
415    pub fn has_domain_data(&self) -> bool {
416        self.identity.is_some()
417            || self.location.is_some()
418            || self.motion.is_some()
419            || self.power.is_some()
420            || self.operational.is_some()
421            || self.navigation.is_some()
422            || self.perception.is_some()
423            || self.safety.is_some()
424            || self.actuators.is_some()
425            || self.communication.is_some()
426            || self.compute.is_some()
427            || self.ai.is_some()
428            || self.maintenance.is_some()
429            || self.context.is_some()
430            || self.payload.is_some()
431            || self.manipulation.is_some()
432            || self.hri.is_some()
433            || self.coordination.is_some()
434            || self.simulation.is_some()
435            || self.thermal.is_some()
436            || self.audio.is_some()
437            || self.environment_interaction.is_some()
438            || self.compliance.is_some()
439    }
440
441    /// Get estimated size in bytes (serialized JSON).
442    pub fn estimated_size(&self) -> usize {
443        serde_json::to_string(self).map(|s| s.len()).unwrap_or(0)
444    }
445
446    /// Serialize to JSON string.
447    pub fn to_json(&self) -> Result<String, serde_json::Error> {
448        serde_json::to_string(self)
449    }
450
451    /// Serialize to pretty JSON string.
452    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
453        serde_json::to_string_pretty(self)
454    }
455
456    /// Deserialize from JSON string.
457    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
458        serde_json::from_str(json)
459    }
460}
461
462impl Provenance {
463    /// Create new provenance with algorithm.
464    pub fn new(algorithm: impl Into<String>) -> Self {
465        Self {
466            algorithm: Some(algorithm.into()),
467            signed_at: Some(Utc::now()),
468            ..Default::default()
469        }
470    }
471
472    /// Set the signature.
473    pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
474        self.signature = Some(signature.into());
475        self
476    }
477
478    /// Set the key ID.
479    pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
480        self.key_id = Some(key_id.into());
481        self
482    }
483
484    /// Set signed fields.
485    pub fn with_signed_fields(mut self, fields: Vec<String>) -> Self {
486        self.signed_fields = Some(fields);
487        self
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_event_creation() {
497        let event = UdmEvent::new(SourceType::Amr);
498
499        assert!(!event.event_id.is_empty());
500        assert_eq!(event.source_type, SourceType::Amr);
501        assert_eq!(event.event_type, EventType::TelemetryPeriodic);
502        assert!(!event.has_domain_data());
503    }
504
505    #[test]
506    fn test_event_with_domains() {
507        use crate::models::domains::location::LocalCoordinates;
508
509        let event = UdmEvent::new(SourceType::Amr)
510            .with_identity(IdentityDomain {
511                source_id: Some("robot-001".to_string()),
512                ..Default::default()
513            })
514            .with_location(LocationDomain {
515                local: Some(LocalCoordinates {
516                    x_m: Some(10.0),
517                    y_m: Some(20.0),
518                    ..Default::default()
519                }),
520                ..Default::default()
521            });
522
523        assert!(event.has_domain_data());
524        assert_eq!(
525            event.identity.as_ref().unwrap().source_id,
526            Some("robot-001".to_string())
527        );
528    }
529
530    #[test]
531    fn test_event_serialization() {
532        let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::StateTransition);
533
534        let json = event.to_json().unwrap();
535        assert!(json.contains("\"event_id\""));
536        assert!(json.contains("\"state_transition\""));
537
538        let deserialized = UdmEvent::from_json(&json).unwrap();
539        assert_eq!(deserialized.event_type, EventType::StateTransition);
540    }
541
542    #[test]
543    fn test_provenance() {
544        let provenance = Provenance::new("hmac-sha256")
545            .with_signature("abc123")
546            .with_key_id("key-001");
547
548        assert_eq!(provenance.algorithm, Some("hmac-sha256".to_string()));
549        assert_eq!(provenance.signature, Some("abc123".to_string()));
550    }
551
552    #[test]
553    fn test_event_with_extensions() {
554        let extensions = serde_json::json!({
555            "custom_field": "custom_value",
556            "nested": { "key": 42 }
557        });
558
559        let event = UdmEvent::new(SourceType::Custom).with_extensions(extensions.clone());
560
561        assert_eq!(event.extensions, Some(extensions));
562    }
563
564    // ==========================================================================
565    // UdmEvent Additional Tests
566    // ==========================================================================
567
568    #[test]
569    fn test_event_default() {
570        let event = UdmEvent::default();
571        assert!(event.event_id.is_empty());
572        assert!(event.identity.is_none());
573        assert!(event.location.is_none());
574    }
575
576    #[test]
577    fn test_event_with_event_type() {
578        let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::SafetyViolation);
579        assert_eq!(event.event_type, EventType::SafetyViolation);
580    }
581
582    #[test]
583    fn test_event_with_all_domains() {
584        let event = UdmEvent::new(SourceType::Amr)
585            .with_identity(IdentityDomain::default())
586            .with_location(LocationDomain::default())
587            .with_motion(MotionDomain::default())
588            .with_power(PowerDomain::default())
589            .with_operational(OperationalDomain::default())
590            .with_navigation(NavigationDomain::default())
591            .with_perception(PerceptionDomain::default())
592            .with_safety(SafetyDomain::default())
593            .with_actuators(ActuatorsDomain::default())
594            .with_communication(CommunicationDomain::default())
595            .with_compute(ComputeDomain::default())
596            .with_ai(AiDomain::default())
597            .with_maintenance(MaintenanceDomain::default())
598            .with_context(ContextDomain::default())
599            .with_payload(PayloadDomain::default())
600            .with_manipulation(ManipulationDomain::default())
601            .with_hri(HriDomain::default())
602            .with_coordination(CoordinationDomain::default())
603            .with_simulation(SimulationDomain::default())
604            .with_thermal(ThermalDomain::default())
605            .with_audio(AudioDomain::default())
606            .with_environment_interaction(EnvironmentInteractionDomain::default())
607            .with_compliance(ComplianceDomain::default());
608
609        assert!(event.has_domain_data());
610        assert!(event.identity.is_some());
611        assert!(event.location.is_some());
612        assert!(event.motion.is_some());
613        assert!(event.power.is_some());
614        assert!(event.operational.is_some());
615        assert!(event.navigation.is_some());
616        assert!(event.perception.is_some());
617        assert!(event.safety.is_some());
618        assert!(event.actuators.is_some());
619        assert!(event.communication.is_some());
620        assert!(event.compute.is_some());
621        assert!(event.ai.is_some());
622        assert!(event.maintenance.is_some());
623        assert!(event.context.is_some());
624        assert!(event.payload.is_some());
625        assert!(event.manipulation.is_some());
626        assert!(event.hri.is_some());
627        assert!(event.coordination.is_some());
628        assert!(event.simulation.is_some());
629        assert!(event.thermal.is_some());
630        assert!(event.audio.is_some());
631        assert!(event.environment_interaction.is_some());
632        assert!(event.compliance.is_some());
633    }
634
635    #[test]
636    fn test_event_with_provenance() {
637        let prov = Provenance::new("hmac-sha256")
638            .with_signature("sig123")
639            .with_key_id("key-001");
640
641        let event = UdmEvent::new(SourceType::Amr).with_provenance(prov);
642
643        assert!(event.provenance.is_some());
644        assert_eq!(
645            event.provenance.as_ref().unwrap().signature,
646            Some("sig123".to_string())
647        );
648    }
649
650    #[test]
651    fn test_event_has_domain_data_single() {
652        // Test each domain individually
653        let event_identity =
654            UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain::default());
655        assert!(event_identity.has_domain_data());
656
657        let event_motion = UdmEvent::new(SourceType::Amr).with_motion(MotionDomain::default());
658        assert!(event_motion.has_domain_data());
659
660        let event_ai = UdmEvent::new(SourceType::Amr).with_ai(AiDomain::default());
661        assert!(event_ai.has_domain_data());
662    }
663
664    #[test]
665    fn test_event_estimated_size() {
666        let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
667            source_id: Some("robot-001".to_string()),
668            ..Default::default()
669        });
670
671        let size = event.estimated_size();
672        assert!(size > 0);
673    }
674
675    #[test]
676    fn test_event_to_json() {
677        let event = UdmEvent::new(SourceType::Amr);
678        let json = event.to_json().unwrap();
679
680        assert!(json.contains("event_id"));
681        assert!(json.contains("captured_at"));
682        assert!(json.contains("source_type"));
683    }
684
685    #[test]
686    fn test_event_to_json_pretty() {
687        let event = UdmEvent::new(SourceType::Amr);
688        let json = event.to_json_pretty().unwrap();
689
690        // Pretty JSON contains newlines
691        assert!(json.contains("\n"));
692    }
693
694    #[test]
695    fn test_event_from_json() {
696        let event = UdmEvent::new(SourceType::Drone);
697        let json = event.to_json().unwrap();
698
699        let deserialized = UdmEvent::from_json(&json).unwrap();
700        assert_eq!(deserialized.source_type, SourceType::Drone);
701    }
702
703    #[test]
704    fn test_event_from_json_invalid() {
705        let result = UdmEvent::from_json("invalid json");
706        assert!(result.is_err());
707    }
708
709    // ==========================================================================
710    // Provenance Additional Tests
711    // ==========================================================================
712
713    #[test]
714    fn test_provenance_default() {
715        let prov = Provenance::default();
716        assert!(prov.signature.is_none());
717        assert!(prov.key_id.is_none());
718        assert!(prov.algorithm.is_none());
719        assert!(prov.signed_fields.is_none());
720        assert!(prov.signed_at.is_none());
721    }
722
723    #[test]
724    fn test_provenance_new() {
725        let prov = Provenance::new("hmac-sha256");
726        assert_eq!(prov.algorithm, Some("hmac-sha256".to_string()));
727        assert!(prov.signed_at.is_some());
728    }
729
730    #[test]
731    fn test_provenance_with_signature() {
732        let prov = Provenance::new("hmac-sha256").with_signature("abc123def456");
733        assert_eq!(prov.signature, Some("abc123def456".to_string()));
734    }
735
736    #[test]
737    fn test_provenance_with_key_id() {
738        let prov = Provenance::new("hmac-sha256").with_key_id("my-key-001");
739        assert_eq!(prov.key_id, Some("my-key-001".to_string()));
740    }
741
742    #[test]
743    fn test_provenance_with_signed_fields() {
744        let fields = vec![
745            "event_id".to_string(),
746            "timestamp".to_string(),
747            "identity".to_string(),
748        ];
749        let prov = Provenance::new("hmac-sha256").with_signed_fields(fields.clone());
750        assert_eq!(prov.signed_fields, Some(fields));
751    }
752
753    #[test]
754    fn test_provenance_chained_builders() {
755        let prov = Provenance::new("hmac-sha256")
756            .with_signature("sig")
757            .with_key_id("key")
758            .with_signed_fields(vec!["event_id".to_string()]);
759
760        assert!(prov.algorithm.is_some());
761        assert!(prov.signature.is_some());
762        assert!(prov.key_id.is_some());
763        assert!(prov.signed_fields.is_some());
764    }
765
766    // ==========================================================================
767    // Serialization Roundtrip Tests
768    // ==========================================================================
769
770    #[test]
771    fn test_event_full_serialization_roundtrip() {
772        let event = UdmEvent::new(SourceType::Amr)
773            .with_event_type(EventType::TelemetryPeriodic)
774            .with_identity(IdentityDomain {
775                source_id: Some("robot-001".to_string()),
776                ..Default::default()
777            })
778            .with_location(LocationDomain::default())
779            .with_provenance(Provenance::new("hmac-sha256").with_signature("test"));
780
781        let json = event.to_json().unwrap();
782        let deserialized = UdmEvent::from_json(&json).unwrap();
783
784        assert_eq!(deserialized.source_type, SourceType::Amr);
785        assert!(deserialized.identity.is_some());
786        assert!(deserialized.location.is_some());
787        assert!(deserialized.provenance.is_some());
788    }
789
790    #[test]
791    fn test_event_clone() {
792        let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
793            source_id: Some("robot-001".to_string()),
794            ..Default::default()
795        });
796
797        let cloned = event.clone();
798        assert_eq!(cloned.event_id, event.event_id);
799        assert_eq!(cloned.source_type, event.source_type);
800    }
801}