1use crate::id::*;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub type EventPayload = aios_protocol::EventKind;
18
19pub use aios_protocol::MemoryScope;
22pub use aios_protocol::event::{
23 ApprovalDecision, PolicyDecisionKind, RiskLevel, SnapshotType, SpanStatus, TokenUsage,
24};
25
26#[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 #[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 #[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); 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 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()); 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 #[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")); 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 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 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 set.insert(MemoryScope::Session);
731 assert_eq!(set.len(), 4);
732 }
733
734 #[test]
735 fn existing_variants_still_work_after_memory_addition() {
736 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 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 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 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 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 #[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 #[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 #[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 #[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}