Skip to main content

lago_core/
event.rs

1//! Event types for Lago.
2//!
3//! As of this version, Lago uses `aios_protocol::EventKind` as the canonical
4//! payload type for all events. The `EventPayload` type alias preserves
5//! backward compatibility with existing code that references `EventPayload`.
6
7use crate::id::*;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11// ─── Re-export the canonical event payload from aios-protocol ──────────────
12
13/// The event payload type used by Lago.
14///
15/// This is the canonical `aios_protocol::EventKind` — all events stored
16/// in Lago's journal use the Agent OS protocol types directly.
17pub type EventPayload = aios_protocol::EventKind;
18
19// ─── Re-export supporting types from aios-protocol ─────────────────────────
20
21pub use aios_protocol::MemoryScope;
22pub use aios_protocol::event::{
23    ApprovalDecision, PolicyDecisionKind, RiskLevel, SnapshotType, SpanStatus, TokenUsage,
24};
25
26// ─── EventEnvelope ─────────────────────────────────────────────────────────
27
28/// The universal unit of state change in Lago.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct EventEnvelope {
31    pub event_id: EventId,
32    pub session_id: SessionId,
33    pub branch_id: BranchId,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub run_id: Option<RunId>,
36    pub seq: SeqNo,
37    pub timestamp: u64,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub parent_id: Option<EventId>,
40    pub payload: EventPayload,
41    #[serde(default)]
42    pub metadata: HashMap<String, String>,
43    /// Schema version for forward compatibility. Defaults to 1.
44    #[serde(default = "default_schema_version")]
45    pub schema_version: u8,
46}
47
48fn default_schema_version() -> u8 {
49    1
50}
51
52impl EventEnvelope {
53    pub fn now_micros() -> u64 {
54        std::time::SystemTime::now()
55            .duration_since(std::time::UNIX_EPOCH)
56            .unwrap_or_default()
57            .as_micros() as u64
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    fn make_test_envelope(payload: EventPayload) -> EventEnvelope {
66        EventEnvelope {
67            event_id: EventId::from_string("EVT001"),
68            session_id: SessionId::from_string("SESS001"),
69            branch_id: BranchId::from_string("main"),
70            run_id: None,
71            seq: 1,
72            timestamp: 1_700_000_000_000_000,
73            parent_id: None,
74            payload,
75            metadata: HashMap::new(),
76            schema_version: 1,
77        }
78    }
79
80    #[test]
81    fn now_micros_returns_nonzero() {
82        let ts = EventEnvelope::now_micros();
83        assert!(ts > 0);
84    }
85
86    #[test]
87    fn now_micros_is_monotonic() {
88        let a = EventEnvelope::now_micros();
89        let b = EventEnvelope::now_micros();
90        assert!(b >= a);
91    }
92
93    #[test]
94    fn message_payload_serde_roundtrip() {
95        let payload = EventPayload::Message {
96            role: "assistant".to_string(),
97            content: "Hello, world!".to_string(),
98            model: Some("gpt-4".to_string()),
99            token_usage: Some(TokenUsage {
100                prompt_tokens: 10,
101                completion_tokens: 20,
102                total_tokens: 30,
103            }),
104        };
105        let json = serde_json::to_string(&payload).unwrap();
106        assert!(json.contains("\"type\":\"Message\""));
107        assert!(json.contains("\"role\":\"assistant\""));
108        let back: EventPayload = serde_json::from_str(&json).unwrap();
109        if let EventPayload::Message { role, content, .. } = back {
110            assert_eq!(role, "assistant");
111            assert_eq!(content, "Hello, world!");
112        } else {
113            panic!("deserialized to wrong variant");
114        }
115    }
116
117    #[test]
118    fn message_delta_serde_roundtrip() {
119        let payload = EventPayload::TextDelta {
120            delta: "chunk".to_string(),
121            index: Some(3),
122        };
123        let json = serde_json::to_string(&payload).unwrap();
124        let back: EventPayload = serde_json::from_str(&json).unwrap();
125        if let EventPayload::TextDelta { delta, index, .. } = back {
126            assert_eq!(delta, "chunk");
127            assert_eq!(index, Some(3));
128        } else {
129            panic!("deserialized to wrong variant");
130        }
131    }
132
133    #[test]
134    fn file_write_serde_roundtrip() {
135        let payload = EventPayload::FileWrite {
136            path: "/src/main.rs".to_string(),
137            blob_hash: BlobHash::from_hex("abcdef").into(),
138            size_bytes: 1024,
139            content_type: Some("text/rust".to_string()),
140        };
141        let json = serde_json::to_string(&payload).unwrap();
142        let back: EventPayload = serde_json::from_str(&json).unwrap();
143        if let EventPayload::FileWrite {
144            path,
145            blob_hash,
146            size_bytes,
147            ..
148        } = back
149        {
150            assert_eq!(path, "/src/main.rs");
151            assert_eq!(blob_hash.as_str(), "abcdef");
152            assert_eq!(size_bytes, 1024);
153        } else {
154            panic!("deserialized to wrong variant");
155        }
156    }
157
158    #[test]
159    fn tool_invoke_result_serde_roundtrip() {
160        let invoke = EventPayload::ToolCallRequested {
161            call_id: "call-1".to_string(),
162            tool_name: "read_file".to_string(),
163            arguments: serde_json::json!({"path": "/etc/hosts"}),
164            category: Some("fs".to_string()),
165        };
166        let result = EventPayload::ToolCallCompleted {
167            tool_run_id: aios_protocol::ToolRunId::default(),
168            call_id: Some("call-1".to_string()),
169            tool_name: "read_file".to_string(),
170            result: serde_json::json!({"content": "127.0.0.1 localhost"}),
171            duration_ms: 42,
172            status: SpanStatus::Ok,
173        };
174        let j1 = serde_json::to_string(&invoke).unwrap();
175        let j2 = serde_json::to_string(&result).unwrap();
176        assert!(j1.contains("\"type\":\"ToolCallRequested\""));
177        assert!(j2.contains("\"type\":\"ToolCallCompleted\""));
178
179        let back: EventPayload = serde_json::from_str(&j2).unwrap();
180        if let EventPayload::ToolCallCompleted { status, .. } = back {
181            assert_eq!(status, SpanStatus::Ok);
182        } else {
183            panic!("wrong variant");
184        }
185    }
186
187    #[test]
188    fn approval_serde_roundtrip() {
189        let requested = EventPayload::ApprovalRequested {
190            approval_id: ApprovalId::from_string("APR001").into(),
191            call_id: "call-1".to_string(),
192            tool_name: "rm".to_string(),
193            arguments: serde_json::json!({"path": "/"}),
194            risk: RiskLevel::Critical,
195        };
196        let json = serde_json::to_string(&requested).unwrap();
197        assert!(json.contains("\"risk\":\"critical\""));
198
199        let resolved = EventPayload::ApprovalResolved {
200            approval_id: ApprovalId::from_string("APR001").into(),
201            decision: ApprovalDecision::Denied,
202            reason: Some("too dangerous".to_string()),
203        };
204        let j2 = serde_json::to_string(&resolved).unwrap();
205        assert!(j2.contains("\"decision\":\"denied\""));
206    }
207
208    #[test]
209    fn branch_events_serde_roundtrip() {
210        let created = EventPayload::BranchCreated {
211            new_branch_id: BranchId::from_string("feature-x").into(),
212            fork_point_seq: 42,
213            name: "feature-x".to_string(),
214        };
215        let json = serde_json::to_string(&created).unwrap();
216        let back: EventPayload = serde_json::from_str(&json).unwrap();
217        if let EventPayload::BranchCreated { fork_point_seq, .. } = back {
218            assert_eq!(fork_point_seq, 42);
219        } else {
220            panic!("wrong variant");
221        }
222    }
223
224    #[test]
225    fn custom_event_serde_roundtrip() {
226        let custom = EventPayload::Custom {
227            event_type: "my.custom.event".to_string(),
228            data: serde_json::json!({"key": "value"}),
229        };
230        let json = serde_json::to_string(&custom).unwrap();
231        let back: EventPayload = serde_json::from_str(&json).unwrap();
232        if let EventPayload::Custom { event_type, data } = back {
233            assert_eq!(event_type, "my.custom.event");
234            assert_eq!(data["key"], "value");
235        } else {
236            panic!("wrong variant");
237        }
238    }
239
240    #[test]
241    fn full_envelope_serde_roundtrip() {
242        let envelope = EventEnvelope {
243            event_id: EventId::from_string("EVT001"),
244            session_id: SessionId::from_string("SESS001"),
245            branch_id: BranchId::from_string("main"),
246            run_id: Some(RunId::from_string("RUN001")),
247            seq: 42,
248            timestamp: 1_700_000_000_000_000,
249            parent_id: Some(EventId::from_string("EVT000")),
250            payload: EventPayload::Message {
251                role: "user".to_string(),
252                content: "hi".to_string(),
253                model: None,
254                token_usage: None,
255            },
256            metadata: HashMap::from([("key".to_string(), "val".to_string())]),
257            schema_version: 1,
258        };
259        let json = serde_json::to_string(&envelope).unwrap();
260        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
261        assert_eq!(back.seq, 42);
262        assert_eq!(back.event_id.as_str(), "EVT001");
263        assert_eq!(back.run_id.as_ref().unwrap().as_str(), "RUN001");
264        assert_eq!(back.parent_id.as_ref().unwrap().as_str(), "EVT000");
265        assert_eq!(back.metadata["key"], "val");
266    }
267
268    #[test]
269    fn envelope_optional_fields_skip_when_none() {
270        let envelope = make_test_envelope(EventPayload::FileDelete {
271            path: "/tmp/test".to_string(),
272        });
273        let json = serde_json::to_string(&envelope).unwrap();
274        assert!(!json.contains("run_id"));
275        assert!(!json.contains("parent_id"));
276    }
277
278    #[test]
279    fn span_status_serde() {
280        assert_eq!(serde_json::to_string(&SpanStatus::Ok).unwrap(), "\"ok\"");
281        assert_eq!(
282            serde_json::to_string(&SpanStatus::Error).unwrap(),
283            "\"error\""
284        );
285        assert_eq!(
286            serde_json::to_string(&SpanStatus::Timeout).unwrap(),
287            "\"timeout\""
288        );
289        assert_eq!(
290            serde_json::to_string(&SpanStatus::Cancelled).unwrap(),
291            "\"cancelled\""
292        );
293    }
294
295    #[test]
296    fn risk_level_serde() {
297        assert_eq!(serde_json::to_string(&RiskLevel::Low).unwrap(), "\"low\"");
298        assert_eq!(
299            serde_json::to_string(&RiskLevel::Critical).unwrap(),
300            "\"critical\""
301        );
302    }
303
304    #[test]
305    fn snapshot_type_serde() {
306        assert_eq!(
307            serde_json::to_string(&SnapshotType::Full).unwrap(),
308            "\"full\""
309        );
310        assert_eq!(
311            serde_json::to_string(&SnapshotType::Incremental).unwrap(),
312            "\"incremental\""
313        );
314    }
315
316    #[test]
317    fn policy_decision_kind_serde() {
318        assert_eq!(
319            serde_json::to_string(&PolicyDecisionKind::RequireApproval).unwrap(),
320            "\"require_approval\""
321        );
322    }
323
324    #[test]
325    fn sandbox_created_serde_roundtrip() {
326        let payload = EventPayload::SandboxCreated {
327            sandbox_id: "sbx-001".to_string(),
328            tier: "container".to_string(),
329            config: serde_json::json!({
330                "tier": "container",
331                "allowed_paths": ["/workspace"],
332                "allowed_commands": ["cargo"],
333                "network_access": false,
334                "max_memory_mb": 512,
335                "max_cpu_seconds": 60,
336            }),
337        };
338        let json = serde_json::to_string(&payload).unwrap();
339        assert!(json.contains("\"type\":\"SandboxCreated\""));
340        let back: EventPayload = serde_json::from_str(&json).unwrap();
341        if let EventPayload::SandboxCreated {
342            sandbox_id, tier, ..
343        } = back
344        {
345            assert_eq!(sandbox_id, "sbx-001");
346            assert_eq!(tier, "container");
347        } else {
348            panic!("wrong variant");
349        }
350    }
351
352    #[test]
353    fn sandbox_executed_serde_roundtrip() {
354        let payload = EventPayload::SandboxExecuted {
355            sandbox_id: "sbx-001".to_string(),
356            command: "cargo test".to_string(),
357            exit_code: 0,
358            duration_ms: 1234,
359        };
360        let json = serde_json::to_string(&payload).unwrap();
361        let back: EventPayload = serde_json::from_str(&json).unwrap();
362        if let EventPayload::SandboxExecuted {
363            exit_code,
364            duration_ms,
365            ..
366        } = back
367        {
368            assert_eq!(exit_code, 0);
369            assert_eq!(duration_ms, 1234);
370        } else {
371            panic!("wrong variant");
372        }
373    }
374
375    #[test]
376    fn sandbox_violation_serde_roundtrip() {
377        let payload = EventPayload::SandboxViolation {
378            sandbox_id: "sbx-001".to_string(),
379            violation_type: "network_access".to_string(),
380            details: "attempted outbound connection to 1.2.3.4:443".to_string(),
381        };
382        let json = serde_json::to_string(&payload).unwrap();
383        let back: EventPayload = serde_json::from_str(&json).unwrap();
384        if let EventPayload::SandboxViolation {
385            violation_type,
386            details,
387            ..
388        } = back
389        {
390            assert_eq!(violation_type, "network_access");
391            assert!(details.contains("1.2.3.4"));
392        } else {
393            panic!("wrong variant");
394        }
395    }
396
397    #[test]
398    fn sandbox_destroyed_serde_roundtrip() {
399        let payload = EventPayload::SandboxDestroyed {
400            sandbox_id: "sbx-001".to_string(),
401        };
402        let json = serde_json::to_string(&payload).unwrap();
403        let back: EventPayload = serde_json::from_str(&json).unwrap();
404        if let EventPayload::SandboxDestroyed { sandbox_id } = back {
405            assert_eq!(sandbox_id, "sbx-001");
406        } else {
407            panic!("wrong variant");
408        }
409    }
410
411    // --- Forward compatibility tests
412
413    #[test]
414    fn unknown_variant_deserializes_as_custom() {
415        let json = r#"{"type":"VisionResult","image_hash":"abc123","confidence":0.95}"#;
416        let payload: EventPayload = serde_json::from_str(json).unwrap();
417        if let EventPayload::Custom { event_type, data } = payload {
418            assert_eq!(event_type, "VisionResult");
419            assert_eq!(data["image_hash"], "abc123");
420            assert_eq!(data["confidence"], 0.95);
421        } else {
422            panic!("unknown variant should deserialize as Custom");
423        }
424    }
425
426    #[test]
427    fn unknown_variant_with_nested_objects() {
428        let json =
429            r#"{"type":"FutureAgent","config":{"model":"x","params":[1,2,3]},"active":true}"#;
430        let payload: EventPayload = serde_json::from_str(json).unwrap();
431        if let EventPayload::Custom { event_type, data } = payload {
432            assert_eq!(event_type, "FutureAgent");
433            assert_eq!(data["config"]["model"], "x");
434            assert_eq!(data["active"], true);
435        } else {
436            panic!("unknown variant should deserialize as Custom");
437        }
438    }
439
440    #[test]
441    fn unknown_variant_in_full_envelope() {
442        let json = r#"{
443            "event_id": "EVT999",
444            "session_id": "SESS001",
445            "branch_id": "main",
446            "seq": 100,
447            "timestamp": 1700000000000000,
448            "payload": {"type":"NewFeature","value":42},
449            "metadata": {}
450        }"#;
451        let envelope: EventEnvelope = serde_json::from_str(json).unwrap();
452        assert_eq!(envelope.event_id.as_str(), "EVT999");
453        assert_eq!(envelope.schema_version, 1); // default
454        if let EventPayload::Custom { event_type, data } = &envelope.payload {
455            assert_eq!(event_type, "NewFeature");
456            assert_eq!(data["value"], 42);
457        } else {
458            panic!("unknown variant in envelope should deserialize as Custom");
459        }
460    }
461
462    #[test]
463    fn known_variants_still_deserialize_correctly() {
464        // Verify the custom deserializer doesn't break normal operation
465        let json = r#"{"type":"FileDelete","path":"/tmp/test"}"#;
466        let payload: EventPayload = serde_json::from_str(json).unwrap();
467        assert!(matches!(payload, EventPayload::FileDelete { .. }));
468
469        let json = r#"{"type":"Message","role":"user","content":"hi"}"#;
470        let payload: EventPayload = serde_json::from_str(json).unwrap();
471        if let EventPayload::Message {
472            role,
473            content,
474            model,
475            token_usage,
476        } = payload
477        {
478            assert_eq!(role, "user");
479            assert_eq!(content, "hi");
480            assert!(model.is_none()); // default for missing optional
481            assert!(token_usage.is_none());
482        } else {
483            panic!("known variant should deserialize normally");
484        }
485    }
486
487    #[test]
488    fn schema_version_defaults_to_1() {
489        let json = r#"{
490            "event_id": "E1",
491            "session_id": "S1",
492            "branch_id": "main",
493            "seq": 0,
494            "timestamp": 100,
495            "payload": {"type":"Error","error":"boom"},
496            "metadata": {}
497        }"#;
498        let envelope: EventEnvelope = serde_json::from_str(json).unwrap();
499        assert_eq!(envelope.schema_version, 1);
500    }
501
502    #[test]
503    fn schema_version_roundtrips() {
504        let envelope = make_test_envelope(EventPayload::ErrorRaised {
505            message: "test".to_string(),
506        });
507        let json = serde_json::to_string(&envelope).unwrap();
508        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
509        assert_eq!(back.schema_version, 1);
510    }
511
512    // --- Memory event tests
513
514    #[test]
515    fn memory_scope_serde_roundtrip() {
516        for (scope, expected) in [
517            (MemoryScope::Session, "\"session\""),
518            (MemoryScope::User, "\"user\""),
519            (MemoryScope::Agent, "\"agent\""),
520            (MemoryScope::Org, "\"org\""),
521        ] {
522            let json = serde_json::to_string(&scope).unwrap();
523            assert_eq!(json, expected);
524            let back: MemoryScope = serde_json::from_str(&json).unwrap();
525            assert_eq!(scope, back);
526        }
527    }
528
529    #[test]
530    fn memory_id_uniqueness() {
531        let a = MemoryId::new();
532        let b = MemoryId::new();
533        assert_ne!(a, b);
534    }
535
536    #[test]
537    fn memory_id_serde_roundtrip() {
538        let id = MemoryId::from_string("MEM001");
539        let json = serde_json::to_string(&id).unwrap();
540        assert_eq!(json, "\"MEM001\"");
541        let back: MemoryId = serde_json::from_str(&json).unwrap();
542        assert_eq!(id, back);
543    }
544
545    #[test]
546    fn observation_appended_serde_roundtrip() {
547        let payload = EventPayload::ObservationAppended {
548            scope: MemoryScope::Session,
549            observation_ref: BlobHash::from_hex("abc123").into(),
550            source_run_id: Some("run-1".to_string()),
551        };
552        let json = serde_json::to_string(&payload).unwrap();
553        assert!(json.contains("\"type\":\"ObservationAppended\""));
554        assert!(json.contains("\"scope\":\"session\""));
555        let back: EventPayload = serde_json::from_str(&json).unwrap();
556        if let EventPayload::ObservationAppended {
557            scope,
558            observation_ref,
559            source_run_id,
560        } = back
561        {
562            assert_eq!(scope, MemoryScope::Session);
563            assert_eq!(observation_ref.as_str(), "abc123");
564            assert_eq!(source_run_id.as_deref(), Some("run-1"));
565        } else {
566            panic!("wrong variant");
567        }
568    }
569
570    #[test]
571    fn reflection_compacted_serde_roundtrip() {
572        let payload = EventPayload::ReflectionCompacted {
573            scope: MemoryScope::User,
574            summary_ref: BlobHash::from_hex("def456").into(),
575            covers_through_seq: 42,
576        };
577        let json = serde_json::to_string(&payload).unwrap();
578        assert!(json.contains("\"type\":\"ReflectionCompacted\""));
579        let back: EventPayload = serde_json::from_str(&json).unwrap();
580        if let EventPayload::ReflectionCompacted {
581            scope,
582            summary_ref,
583            covers_through_seq,
584        } = back
585        {
586            assert_eq!(scope, MemoryScope::User);
587            assert_eq!(summary_ref.as_str(), "def456");
588            assert_eq!(covers_through_seq, 42);
589        } else {
590            panic!("wrong variant");
591        }
592    }
593
594    #[test]
595    fn memory_proposed_serde_roundtrip() {
596        let payload = EventPayload::MemoryProposed {
597            scope: MemoryScope::Agent,
598            proposal_id: MemoryId::from_string("PROP001").into(),
599            entries_ref: BlobHash::from_hex("789abc").into(),
600            source_run_id: None,
601        };
602        let json = serde_json::to_string(&payload).unwrap();
603        assert!(json.contains("\"type\":\"MemoryProposed\""));
604        assert!(!json.contains("source_run_id")); // None fields are skipped
605        let back: EventPayload = serde_json::from_str(&json).unwrap();
606        if let EventPayload::MemoryProposed {
607            scope,
608            proposal_id,
609            entries_ref,
610            source_run_id,
611        } = back
612        {
613            assert_eq!(scope, MemoryScope::Agent);
614            assert_eq!(proposal_id.as_str(), "PROP001");
615            assert_eq!(entries_ref.as_str(), "789abc");
616            assert!(source_run_id.is_none());
617        } else {
618            panic!("wrong variant");
619        }
620    }
621
622    #[test]
623    fn memory_committed_serde_roundtrip() {
624        let payload = EventPayload::MemoryCommitted {
625            scope: MemoryScope::Org,
626            memory_id: MemoryId::from_string("MEM001").into(),
627            committed_ref: BlobHash::from_hex("deadbeef").into(),
628            supersedes: Some(MemoryId::from_string("MEM000").into()),
629        };
630        let json = serde_json::to_string(&payload).unwrap();
631        assert!(json.contains("\"type\":\"MemoryCommitted\""));
632        let back: EventPayload = serde_json::from_str(&json).unwrap();
633        if let EventPayload::MemoryCommitted {
634            scope,
635            memory_id,
636            committed_ref,
637            supersedes,
638        } = back
639        {
640            assert_eq!(scope, MemoryScope::Org);
641            assert_eq!(memory_id.as_str(), "MEM001");
642            assert_eq!(committed_ref.as_str(), "deadbeef");
643            assert_eq!(supersedes.unwrap().as_str(), "MEM000");
644        } else {
645            panic!("wrong variant");
646        }
647    }
648
649    #[test]
650    fn memory_tombstoned_serde_roundtrip() {
651        let payload = EventPayload::MemoryTombstoned {
652            scope: MemoryScope::Session,
653            memory_id: MemoryId::from_string("MEM001").into(),
654            reason: "stale information".to_string(),
655        };
656        let json = serde_json::to_string(&payload).unwrap();
657        assert!(json.contains("\"type\":\"MemoryTombstoned\""));
658        let back: EventPayload = serde_json::from_str(&json).unwrap();
659        if let EventPayload::MemoryTombstoned {
660            scope,
661            memory_id,
662            reason,
663        } = back
664        {
665            assert_eq!(scope, MemoryScope::Session);
666            assert_eq!(memory_id.as_str(), "MEM001");
667            assert_eq!(reason, "stale information");
668        } else {
669            panic!("wrong variant");
670        }
671    }
672
673    #[test]
674    fn memory_event_full_envelope_roundtrip() {
675        let envelope = make_test_envelope(EventPayload::ObservationAppended {
676            scope: MemoryScope::User,
677            observation_ref: BlobHash::from_hex("cafebabe").into(),
678            source_run_id: Some("run-42".to_string()),
679        });
680        let json = serde_json::to_string(&envelope).unwrap();
681        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
682        assert_eq!(back.event_id.as_str(), "EVT001");
683        assert_eq!(back.seq, 1);
684        if let EventPayload::ObservationAppended {
685            scope,
686            observation_ref,
687            ..
688        } = &back.payload
689        {
690            assert_eq!(*scope, MemoryScope::User);
691            assert_eq!(observation_ref.as_str(), "cafebabe");
692        } else {
693            panic!("wrong variant in envelope");
694        }
695    }
696
697    #[test]
698    fn memory_optional_fields_default_on_missing() {
699        // Deserialize ObservationAppended without source_run_id
700        let json = r#"{"type":"ObservationAppended","scope":"session","observation_ref":"abc"}"#;
701        let payload: EventPayload = serde_json::from_str(json).unwrap();
702        if let EventPayload::ObservationAppended { source_run_id, .. } = payload {
703            assert!(source_run_id.is_none());
704        } else {
705            panic!("wrong variant");
706        }
707
708        // Deserialize MemoryCommitted without supersedes
709        let json =
710            r#"{"type":"MemoryCommitted","scope":"agent","memory_id":"M1","committed_ref":"aaa"}"#;
711        let payload: EventPayload = serde_json::from_str(json).unwrap();
712        if let EventPayload::MemoryCommitted { supersedes, .. } = payload {
713            assert!(supersedes.is_none());
714        } else {
715            panic!("wrong variant");
716        }
717    }
718
719    #[test]
720    fn memory_scope_equality_and_hash() {
721        use std::collections::HashSet;
722        let mut set = HashSet::new();
723        set.insert(MemoryScope::Session);
724        set.insert(MemoryScope::User);
725        set.insert(MemoryScope::Agent);
726        set.insert(MemoryScope::Org);
727        assert_eq!(set.len(), 4);
728        assert!(set.contains(&MemoryScope::Session));
729        // Inserting duplicate
730        set.insert(MemoryScope::Session);
731        assert_eq!(set.len(), 4);
732    }
733
734    #[test]
735    fn existing_variants_still_work_after_memory_addition() {
736        // Verify sandbox variants still roundtrip
737        let payload = EventPayload::SandboxCreated {
738            sandbox_id: "sbx-1".to_string(),
739            tier: "container".to_string(),
740            config: serde_json::json!({
741                "tier": "container",
742                "allowed_paths": [],
743                "allowed_commands": [],
744                "network_access": false,
745            }),
746        };
747        let json = serde_json::to_string(&payload).unwrap();
748        let back: EventPayload = serde_json::from_str(&json).unwrap();
749        assert!(matches!(back, EventPayload::SandboxCreated { .. }));
750
751        // Verify Message still works
752        let payload = EventPayload::Message {
753            role: "user".to_string(),
754            content: "test".to_string(),
755            model: None,
756            token_usage: None,
757        };
758        let json = serde_json::to_string(&payload).unwrap();
759        let back: EventPayload = serde_json::from_str(&json).unwrap();
760        assert!(matches!(back, EventPayload::Message { .. }));
761    }
762
763    #[test]
764    fn unknown_memory_variant_falls_back_to_custom() {
765        // A future memory event type unknown to this code version
766        let json = r#"{"type":"MemoryMerged","scope":"user","source_ids":["M1","M2"]}"#;
767        let payload: EventPayload = serde_json::from_str(json).unwrap();
768        if let EventPayload::Custom { event_type, data } = payload {
769            assert_eq!(event_type, "MemoryMerged");
770            assert_eq!(data["scope"], "user");
771        } else {
772            panic!("unknown memory variant should deserialize as Custom");
773        }
774    }
775
776    #[test]
777    fn unknown_variant_preserves_all_fields() {
778        // Verify zero data loss through the unknown variant path
779        let json = r#"{"type":"X","a":1,"b":"two","c":[3],"d":{"e":true}}"#;
780        let payload: EventPayload = serde_json::from_str(json).unwrap();
781        if let EventPayload::Custom { event_type, data } = &payload {
782            assert_eq!(event_type, "X");
783            assert_eq!(data["a"], 1);
784            assert_eq!(data["b"], "two");
785            assert_eq!(data["c"][0], 3);
786            assert_eq!(data["d"]["e"], true);
787
788            // Re-serialize and verify the Custom event can be read back
789            let re_json = serde_json::to_string(&payload).unwrap();
790            let re_payload: EventPayload = serde_json::from_str(&re_json).unwrap();
791            if let EventPayload::Custom {
792                event_type: et2,
793                data: d2,
794            } = re_payload
795            {
796                assert_eq!(et2, "X");
797                assert_eq!(d2["a"], 1);
798            } else {
799                panic!("re-deserialized should still be Custom");
800            }
801        } else {
802            panic!("should be Custom");
803        }
804    }
805
806    // Old Lago "Error" JSON now deserializes as Custom (canonical name is ErrorRaised)
807    #[test]
808    fn lago_error_variant_backward_compat() {
809        let json = r#"{"type":"Error","error":"boom"}"#;
810        let payload: EventPayload = serde_json::from_str(json).unwrap();
811        assert!(matches!(payload, EventPayload::Custom { .. }));
812    }
813
814    // Canonical ErrorRaised roundtrip
815    #[test]
816    fn error_raised_roundtrip() {
817        let payload = EventPayload::ErrorRaised {
818            message: "boom".to_string(),
819        };
820        let json = serde_json::to_string(&payload).unwrap();
821        assert!(json.contains("\"type\":\"ErrorRaised\""));
822        let back: EventPayload = serde_json::from_str(&json).unwrap();
823        assert!(matches!(back, EventPayload::ErrorRaised { .. }));
824    }
825
826    // Old Lago "ToolInvoke" JSON now deserializes as Custom (canonical name is ToolCallRequested)
827    #[test]
828    fn lago_tool_invoke_backward_compat() {
829        let json =
830            r#"{"type":"ToolInvoke","call_id":"c1","tool_name":"exec","arguments":{"cmd":"ls"}}"#;
831        let payload: EventPayload = serde_json::from_str(json).unwrap();
832        assert!(matches!(payload, EventPayload::Custom { .. }));
833    }
834
835    // Canonical SnapshotCreated roundtrip
836    #[test]
837    fn lago_snapshot_roundtrip() {
838        let payload = EventPayload::SnapshotCreated {
839            snapshot_id: SnapshotId::from_string("SNAP001").into(),
840            snapshot_type: SnapshotType::Full,
841            covers_through_seq: 100,
842            data_hash: BlobHash::from_hex("abc").into(),
843        };
844        let json = serde_json::to_string(&payload).unwrap();
845        assert!(json.contains("\"type\":\"SnapshotCreated\""));
846        let back: EventPayload = serde_json::from_str(&json).unwrap();
847        assert!(matches!(back, EventPayload::SnapshotCreated { .. }));
848    }
849}