Skip to main content

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                if let Some(license_obj) = license_data.as_object() {
400                    for (key, value) in license_obj {
401                        obj.insert(key.clone(), value.clone());
402                    }
403                }
404            }
405        } else {
406            self.extensions = Some(license_data);
407        }
408    }
409
410    /// Set provenance information.
411    pub fn with_provenance(mut self, provenance: Provenance) -> Self {
412        self.provenance = Some(provenance);
413        self
414    }
415
416    /// Check if the event has any domain data.
417    pub fn has_domain_data(&self) -> bool {
418        self.identity.is_some()
419            || self.location.is_some()
420            || self.motion.is_some()
421            || self.power.is_some()
422            || self.operational.is_some()
423            || self.navigation.is_some()
424            || self.perception.is_some()
425            || self.safety.is_some()
426            || self.actuators.is_some()
427            || self.communication.is_some()
428            || self.compute.is_some()
429            || self.ai.is_some()
430            || self.maintenance.is_some()
431            || self.context.is_some()
432            || self.payload.is_some()
433            || self.manipulation.is_some()
434            || self.hri.is_some()
435            || self.coordination.is_some()
436            || self.simulation.is_some()
437            || self.thermal.is_some()
438            || self.audio.is_some()
439            || self.environment_interaction.is_some()
440            || self.compliance.is_some()
441    }
442
443    /// Get estimated size in bytes (serialized JSON).
444    pub fn estimated_size(&self) -> usize {
445        serde_json::to_string(self).map(|s| s.len()).unwrap_or(0)
446    }
447
448    /// Serialize to JSON string.
449    pub fn to_json(&self) -> Result<String, serde_json::Error> {
450        serde_json::to_string(self)
451    }
452
453    /// Serialize to pretty JSON string.
454    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
455        serde_json::to_string_pretty(self)
456    }
457
458    /// Deserialize from JSON string.
459    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
460        serde_json::from_str(json)
461    }
462}
463
464impl Provenance {
465    /// Create new provenance with algorithm.
466    pub fn new(algorithm: impl Into<String>) -> Self {
467        Self {
468            algorithm: Some(algorithm.into()),
469            signed_at: Some(Utc::now()),
470            ..Default::default()
471        }
472    }
473
474    /// Set the signature.
475    pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
476        self.signature = Some(signature.into());
477        self
478    }
479
480    /// Set the key ID.
481    pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
482        self.key_id = Some(key_id.into());
483        self
484    }
485
486    /// Set signed fields.
487    pub fn with_signed_fields(mut self, fields: Vec<String>) -> Self {
488        self.signed_fields = Some(fields);
489        self
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn test_event_creation() {
499        let event = UdmEvent::new(SourceType::Amr);
500
501        assert!(!event.event_id.is_empty());
502        assert_eq!(event.source_type, SourceType::Amr);
503        assert_eq!(event.event_type, EventType::TelemetryPeriodic);
504        assert!(!event.has_domain_data());
505    }
506
507    #[test]
508    fn test_event_with_domains() {
509        use crate::models::domains::location::LocalCoordinates;
510
511        let event = UdmEvent::new(SourceType::Amr)
512            .with_identity(IdentityDomain {
513                source_id: Some("robot-001".to_string()),
514                ..Default::default()
515            })
516            .with_location(LocationDomain {
517                local: Some(LocalCoordinates {
518                    x_m: Some(10.0),
519                    y_m: Some(20.0),
520                    ..Default::default()
521                }),
522                ..Default::default()
523            });
524
525        assert!(event.has_domain_data());
526        assert_eq!(
527            event.identity.as_ref().unwrap().source_id,
528            Some("robot-001".to_string())
529        );
530    }
531
532    #[test]
533    fn test_event_serialization() {
534        let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::StateTransition);
535
536        let json = event.to_json().unwrap();
537        assert!(json.contains("\"event_id\""));
538        assert!(json.contains("\"state_transition\""));
539
540        let deserialized = UdmEvent::from_json(&json).unwrap();
541        assert_eq!(deserialized.event_type, EventType::StateTransition);
542    }
543
544    #[test]
545    fn test_provenance() {
546        let provenance = Provenance::new("hmac-sha256")
547            .with_signature("abc123")
548            .with_key_id("key-001");
549
550        assert_eq!(provenance.algorithm, Some("hmac-sha256".to_string()));
551        assert_eq!(provenance.signature, Some("abc123".to_string()));
552    }
553
554    #[test]
555    fn test_event_with_extensions() {
556        let extensions = serde_json::json!({
557            "custom_field": "custom_value",
558            "nested": { "key": 42 }
559        });
560
561        let event = UdmEvent::new(SourceType::Custom).with_extensions(extensions.clone());
562
563        assert_eq!(event.extensions, Some(extensions));
564    }
565
566    // ==========================================================================
567    // UdmEvent Additional Tests
568    // ==========================================================================
569
570    #[test]
571    fn test_event_default() {
572        let event = UdmEvent::default();
573        assert!(event.event_id.is_empty());
574        assert!(event.identity.is_none());
575        assert!(event.location.is_none());
576    }
577
578    #[test]
579    fn test_event_with_event_type() {
580        let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::SafetyViolation);
581        assert_eq!(event.event_type, EventType::SafetyViolation);
582    }
583
584    #[test]
585    fn test_event_with_all_domains() {
586        let event = UdmEvent::new(SourceType::Amr)
587            .with_identity(IdentityDomain::default())
588            .with_location(LocationDomain::default())
589            .with_motion(MotionDomain::default())
590            .with_power(PowerDomain::default())
591            .with_operational(OperationalDomain::default())
592            .with_navigation(NavigationDomain::default())
593            .with_perception(PerceptionDomain::default())
594            .with_safety(SafetyDomain::default())
595            .with_actuators(ActuatorsDomain::default())
596            .with_communication(CommunicationDomain::default())
597            .with_compute(ComputeDomain::default())
598            .with_ai(AiDomain::default())
599            .with_maintenance(MaintenanceDomain::default())
600            .with_context(ContextDomain::default())
601            .with_payload(PayloadDomain::default())
602            .with_manipulation(ManipulationDomain::default())
603            .with_hri(HriDomain::default())
604            .with_coordination(CoordinationDomain::default())
605            .with_simulation(SimulationDomain::default())
606            .with_thermal(ThermalDomain::default())
607            .with_audio(AudioDomain::default())
608            .with_environment_interaction(EnvironmentInteractionDomain::default())
609            .with_compliance(ComplianceDomain::default());
610
611        assert!(event.has_domain_data());
612        assert!(event.identity.is_some());
613        assert!(event.location.is_some());
614        assert!(event.motion.is_some());
615        assert!(event.power.is_some());
616        assert!(event.operational.is_some());
617        assert!(event.navigation.is_some());
618        assert!(event.perception.is_some());
619        assert!(event.safety.is_some());
620        assert!(event.actuators.is_some());
621        assert!(event.communication.is_some());
622        assert!(event.compute.is_some());
623        assert!(event.ai.is_some());
624        assert!(event.maintenance.is_some());
625        assert!(event.context.is_some());
626        assert!(event.payload.is_some());
627        assert!(event.manipulation.is_some());
628        assert!(event.hri.is_some());
629        assert!(event.coordination.is_some());
630        assert!(event.simulation.is_some());
631        assert!(event.thermal.is_some());
632        assert!(event.audio.is_some());
633        assert!(event.environment_interaction.is_some());
634        assert!(event.compliance.is_some());
635    }
636
637    #[test]
638    fn test_event_with_provenance() {
639        let prov = Provenance::new("hmac-sha256")
640            .with_signature("sig123")
641            .with_key_id("key-001");
642
643        let event = UdmEvent::new(SourceType::Amr).with_provenance(prov);
644
645        assert!(event.provenance.is_some());
646        assert_eq!(
647            event.provenance.as_ref().unwrap().signature,
648            Some("sig123".to_string())
649        );
650    }
651
652    #[test]
653    fn test_event_has_domain_data_single() {
654        // Test each domain individually
655        let event_identity =
656            UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain::default());
657        assert!(event_identity.has_domain_data());
658
659        let event_motion = UdmEvent::new(SourceType::Amr).with_motion(MotionDomain::default());
660        assert!(event_motion.has_domain_data());
661
662        let event_ai = UdmEvent::new(SourceType::Amr).with_ai(AiDomain::default());
663        assert!(event_ai.has_domain_data());
664    }
665
666    #[test]
667    fn test_event_estimated_size() {
668        let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
669            source_id: Some("robot-001".to_string()),
670            ..Default::default()
671        });
672
673        let size = event.estimated_size();
674        assert!(size > 0);
675    }
676
677    #[test]
678    fn test_event_to_json() {
679        let event = UdmEvent::new(SourceType::Amr);
680        let json = event.to_json().unwrap();
681
682        assert!(json.contains("event_id"));
683        assert!(json.contains("captured_at"));
684        assert!(json.contains("source_type"));
685    }
686
687    #[test]
688    fn test_event_to_json_pretty() {
689        let event = UdmEvent::new(SourceType::Amr);
690        let json = event.to_json_pretty().unwrap();
691
692        // Pretty JSON contains newlines
693        assert!(json.contains("\n"));
694    }
695
696    #[test]
697    fn test_event_from_json() {
698        let event = UdmEvent::new(SourceType::Drone);
699        let json = event.to_json().unwrap();
700
701        let deserialized = UdmEvent::from_json(&json).unwrap();
702        assert_eq!(deserialized.source_type, SourceType::Drone);
703    }
704
705    #[test]
706    fn test_event_from_json_invalid() {
707        let result = UdmEvent::from_json("invalid json");
708        result.unwrap_err();
709    }
710
711    // ==========================================================================
712    // Provenance Additional Tests
713    // ==========================================================================
714
715    #[test]
716    fn test_provenance_default() {
717        let prov = Provenance::default();
718        assert!(prov.signature.is_none());
719        assert!(prov.key_id.is_none());
720        assert!(prov.algorithm.is_none());
721        assert!(prov.signed_fields.is_none());
722        assert!(prov.signed_at.is_none());
723    }
724
725    #[test]
726    fn test_provenance_new() {
727        let prov = Provenance::new("hmac-sha256");
728        assert_eq!(prov.algorithm, Some("hmac-sha256".to_string()));
729        assert!(prov.signed_at.is_some());
730    }
731
732    #[test]
733    fn test_provenance_with_signature() {
734        let prov = Provenance::new("hmac-sha256").with_signature("abc123def456");
735        assert_eq!(prov.signature, Some("abc123def456".to_string()));
736    }
737
738    #[test]
739    fn test_provenance_with_key_id() {
740        let prov = Provenance::new("hmac-sha256").with_key_id("my-key-001");
741        assert_eq!(prov.key_id, Some("my-key-001".to_string()));
742    }
743
744    #[test]
745    fn test_provenance_with_signed_fields() {
746        let fields = vec![
747            "event_id".to_string(),
748            "timestamp".to_string(),
749            "identity".to_string(),
750        ];
751        let prov = Provenance::new("hmac-sha256").with_signed_fields(fields.clone());
752        assert_eq!(prov.signed_fields, Some(fields));
753    }
754
755    #[test]
756    fn test_provenance_chained_builders() {
757        let prov = Provenance::new("hmac-sha256")
758            .with_signature("sig")
759            .with_key_id("key")
760            .with_signed_fields(vec!["event_id".to_string()]);
761
762        assert!(prov.algorithm.is_some());
763        assert!(prov.signature.is_some());
764        assert!(prov.key_id.is_some());
765        assert!(prov.signed_fields.is_some());
766    }
767
768    // ==========================================================================
769    // Serialization Roundtrip Tests
770    // ==========================================================================
771
772    #[test]
773    fn test_event_full_serialization_roundtrip() {
774        let event = UdmEvent::new(SourceType::Amr)
775            .with_event_type(EventType::TelemetryPeriodic)
776            .with_identity(IdentityDomain {
777                source_id: Some("robot-001".to_string()),
778                ..Default::default()
779            })
780            .with_location(LocationDomain::default())
781            .with_provenance(Provenance::new("hmac-sha256").with_signature("test"));
782
783        let json = event.to_json().unwrap();
784        let deserialized = UdmEvent::from_json(&json).unwrap();
785
786        assert_eq!(deserialized.source_type, SourceType::Amr);
787        assert!(deserialized.identity.is_some());
788        assert!(deserialized.location.is_some());
789        assert!(deserialized.provenance.is_some());
790    }
791
792    #[test]
793    fn test_event_clone() {
794        let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
795            source_id: Some("robot-001".to_string()),
796            ..Default::default()
797        });
798
799        let cloned = event.clone();
800        assert_eq!(cloned.event_id, event.event_id);
801        assert_eq!(cloned.source_type, event.source_type);
802    }
803}