1use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use tracing::debug;
13
14use clawft_core::bus::MessageBus;
15
16use crate::error::KernelError;
17use crate::process::Pid;
18
19pub const KERNEL_IPC_MAX_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
26
27static IPC_MSG_COUNTER: AtomicU64 = AtomicU64::new(1);
34
35#[inline]
39fn next_ipc_msg_id() -> String {
40 let id = IPC_MSG_COUNTER.fetch_add(1, Ordering::Relaxed);
41 format!("ipc-{id}")
42}
43
44#[non_exhaustive]
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub enum MessageTarget {
48 Process(Pid),
50 Topic(String),
52 Broadcast,
54 Service(String),
56 ServiceMethod {
61 service: String,
63 method: String,
65 },
66 Kernel,
68 RemoteNode {
71 node_id: String,
73 target: Box<MessageTarget>,
75 },
76}
77
78#[non_exhaustive]
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum MessagePayload {
82 Text(String),
84 Json(serde_json::Value),
86 ToolCall {
88 name: String,
90 args: serde_json::Value,
92 },
93 ToolResult {
95 call_id: String,
97 result: serde_json::Value,
99 },
100 Signal(KernelSignal),
102 Binary(Vec<u8>),
107 Rvf {
113 segment_type: u8,
115 data: Vec<u8>,
117 },
118}
119
120impl MessagePayload {
121 pub fn type_name(&self) -> &'static str {
123 match self {
124 MessagePayload::Text(_) => "text",
125 MessagePayload::Json(_) => "json",
126 MessagePayload::ToolCall { .. } => "tool_call",
127 MessagePayload::ToolResult { .. } => "tool_result",
128 MessagePayload::Signal(_) => "signal",
129 MessagePayload::Binary(_) => "binary",
130 MessagePayload::Rvf { .. } => "rvf",
131 }
132 }
133}
134
135#[non_exhaustive]
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub enum ExitReason {
139 Normal,
141 Crash(String),
143 Killed,
145 Timeout,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ProcessDown {
152 pub pid: crate::process::Pid,
154 pub reason: ExitReason,
156}
157
158#[non_exhaustive]
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum KernelSignal {
162 Shutdown,
164 Suspend,
166 Resume,
168 Ping,
170 Pong,
172 ReloadConfig,
174 DumpState,
176 UserDefined(u8),
178 Kill,
180 LinkExit {
182 pid: crate::process::Pid,
184 reason: ExitReason,
186 },
187 MonitorDown(ProcessDown),
189 ResourceWarning {
191 resource: String,
193 current: u64,
195 limit: u64,
197 },
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct KernelMessage {
203 pub id: String,
205 pub from: Pid,
207 pub target: MessageTarget,
209 pub payload: MessagePayload,
211 pub timestamp: DateTime<Utc>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub correlation_id: Option<String>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub trace_id: Option<String>,
227}
228
229impl KernelMessage {
230 pub fn new(from: Pid, target: MessageTarget, payload: MessagePayload) -> Self {
235 Self {
236 id: next_ipc_msg_id(),
237 from,
238 target,
239 payload,
240 timestamp: Utc::now(),
241 correlation_id: None,
242 trace_id: None,
243 }
244 }
245
246 pub fn with_correlation(
248 from: Pid,
249 target: MessageTarget,
250 payload: MessagePayload,
251 correlation_id: String,
252 ) -> Self {
253 Self {
254 id: next_ipc_msg_id(),
255 from,
256 target,
257 payload,
258 timestamp: Utc::now(),
259 correlation_id: Some(correlation_id),
260 trace_id: None,
261 }
262 }
263
264 pub fn with_trace(
266 from: Pid,
267 target: MessageTarget,
268 payload: MessagePayload,
269 trace_id: String,
270 ) -> Self {
271 Self {
272 id: next_ipc_msg_id(),
273 from,
274 target,
275 payload,
276 timestamp: Utc::now(),
277 correlation_id: None,
278 trace_id: Some(trace_id),
279 }
280 }
281
282 pub fn set_trace_id(mut self, trace_id: impl Into<String>) -> Self {
284 self.trace_id = Some(trace_id.into());
285 self
286 }
287
288 pub fn ensure_trace_id(mut self) -> Self {
290 if self.trace_id.is_none() {
291 self.trace_id = Some(uuid::Uuid::new_v4().to_string());
292 }
293 self
294 }
295
296 pub fn text(from: Pid, target: MessageTarget, text: impl Into<String>) -> Self {
298 Self::new(from, target, MessagePayload::Text(text.into()))
299 }
300
301 pub fn signal(from: Pid, target: MessageTarget, signal: KernelSignal) -> Self {
303 Self::new(from, target, MessagePayload::Signal(signal))
304 }
305
306 pub fn tool_call(
308 from: Pid,
309 target: MessageTarget,
310 name: impl Into<String>,
311 args: serde_json::Value,
312 ) -> Self {
313 Self::new(
314 from,
315 target,
316 MessagePayload::ToolCall {
317 name: name.into(),
318 args,
319 },
320 )
321 }
322
323 pub fn tool_result(
325 from: Pid,
326 target: MessageTarget,
327 call_id: impl Into<String>,
328 result: serde_json::Value,
329 ) -> Self {
330 Self::new(
331 from,
332 target,
333 MessagePayload::ToolResult {
334 call_id: call_id.into(),
335 result,
336 },
337 )
338 }
339}
340
341pub type MeshNodeId = String;
347
348#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
363pub struct GlobalPid {
364 pub node_id: MeshNodeId,
366 pub pid: Pid,
368}
369
370pub type CompositePid = GlobalPid;
372
373impl GlobalPid {
374 pub fn local(pid: Pid, node_id: &str) -> Self {
376 Self {
377 node_id: node_id.to_string(),
378 pid,
379 }
380 }
381
382 pub fn new(node_id: impl Into<MeshNodeId>, pid: Pid) -> Self {
384 Self {
385 node_id: node_id.into(),
386 pid,
387 }
388 }
389
390 pub fn is_local(&self, my_node_id: &str) -> bool {
392 self.node_id == my_node_id
393 }
394
395 pub fn as_remote_target(&self) -> MessageTarget {
397 MessageTarget::RemoteNode {
398 node_id: self.node_id.clone(),
399 target: Box::new(MessageTarget::Process(self.pid)),
400 }
401 }
402}
403
404impl std::fmt::Display for GlobalPid {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 write!(f, "{}:{}", self.node_id, self.pid)
407 }
408}
409
410pub struct KernelIpc {
415 bus: Arc<MessageBus>,
416}
417
418impl KernelIpc {
419 pub fn new(bus: Arc<MessageBus>) -> Self {
421 Self { bus }
422 }
423
424 pub fn bus(&self) -> &Arc<MessageBus> {
426 &self.bus
427 }
428
429 #[cfg(feature = "exochain")]
436 pub fn send_checked(
437 &self,
438 msg: &KernelMessage,
439 checker: &crate::capability::CapabilityChecker,
440 chain: Option<&crate::chain::ChainManager>,
441 ) -> Result<(), KernelError> {
442 if let MessageTarget::Process(to_pid) = &msg.target {
444 checker.check_ipc_target(msg.from, *to_pid)?;
445 }
446
447 if let Some(cm) = chain {
449 cm.append(
450 "ipc",
451 "ipc.send",
452 Some(serde_json::json!({
453 "from": msg.from,
454 "target": format!("{:?}", msg.target),
455 "payload_type": msg.payload.type_name(),
456 "msg_id": msg.id,
457 })),
458 );
459 }
460
461 self.send(msg)
463 }
464
465 pub fn send(&self, msg: &KernelMessage) -> Result<(), KernelError> {
477 debug!(
478 id = %msg.id,
479 from = msg.from,
480 "sending kernel message"
481 );
482
483 let json = serde_json::to_string(msg)
484 .map_err(|e| KernelError::Ipc(format!("failed to serialize message: {e}")))?;
485
486 if json.len() > KERNEL_IPC_MAX_MESSAGE_BYTES {
490 return Err(KernelError::MessageTooLarge {
491 size: json.len(),
492 limit: KERNEL_IPC_MAX_MESSAGE_BYTES,
493 });
494 }
495
496 let inbound = clawft_types::event::InboundMessage {
499 channel: "kernel-ipc".to_owned(),
500 sender_id: format!("pid-{}", msg.from),
501 chat_id: msg.id.clone(),
502 content: json,
503 timestamp: msg.timestamp,
504 media: vec![],
505 metadata: std::collections::HashMap::new(),
506 };
507
508 self.bus
509 .publish_inbound(inbound)
510 .map_err(|e| KernelError::Ipc(format!("bus publish failed: {e}")))
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn kernel_message_text() {
520 let msg = KernelMessage::text(0, MessageTarget::Process(1), "hello");
521 assert_eq!(msg.from, 0);
522 assert!(matches!(msg.target, MessageTarget::Process(1)));
523 assert!(matches!(msg.payload, MessagePayload::Text(ref t) if t == "hello"));
524 }
525
526 #[test]
527 fn kernel_message_signal() {
528 let msg = KernelMessage::signal(0, MessageTarget::Broadcast, KernelSignal::Shutdown);
529 assert!(matches!(msg.target, MessageTarget::Broadcast));
530 assert!(matches!(
531 msg.payload,
532 MessagePayload::Signal(KernelSignal::Shutdown)
533 ));
534 }
535
536 #[test]
537 fn kernel_message_json_payload() {
538 let payload = MessagePayload::Json(serde_json::json!({"key": "value"}));
539 let msg = KernelMessage::new(1, MessageTarget::Kernel, payload);
540 assert!(matches!(msg.payload, MessagePayload::Json(_)));
541 }
542
543 #[test]
544 fn message_serde_roundtrip() {
545 let msg = KernelMessage::text(5, MessageTarget::Service("health".into()), "check");
546 let json = serde_json::to_string(&msg).unwrap();
547 let restored: KernelMessage = serde_json::from_str(&json).unwrap();
548 assert_eq!(restored.id, msg.id);
549 assert_eq!(restored.from, 5);
550 }
551
552 #[tokio::test]
553 async fn ipc_send() {
554 let bus = Arc::new(MessageBus::new());
555 let ipc = KernelIpc::new(bus.clone());
556
557 let msg = KernelMessage::text(0, MessageTarget::Process(1), "test");
558 ipc.send(&msg).unwrap();
559
560 let received = bus.consume_inbound().await.unwrap();
562 assert_eq!(received.channel, "kernel-ipc");
563 assert_eq!(received.sender_id, "pid-0");
564 }
565
566 #[test]
567 fn ipc_bus_ref() {
568 let bus = Arc::new(MessageBus::new());
569 let ipc = KernelIpc::new(bus.clone());
570 assert!(Arc::ptr_eq(ipc.bus(), &bus));
571 }
572
573 #[test]
574 fn ipc_rejects_oversize_message() {
575 let bus = Arc::new(MessageBus::new());
579 let ipc = KernelIpc::new(bus.clone());
580
581 let huge = vec![0u8; KERNEL_IPC_MAX_MESSAGE_BYTES + 1];
586 let msg = KernelMessage::new(0, MessageTarget::Process(1), MessagePayload::Binary(huge));
587
588 let err = ipc.send(&msg).unwrap_err();
589 match err {
590 KernelError::MessageTooLarge { size, limit } => {
591 assert_eq!(limit, KERNEL_IPC_MAX_MESSAGE_BYTES);
592 assert!(
593 size > KERNEL_IPC_MAX_MESSAGE_BYTES,
594 "expected size > limit, got size={size} limit={limit}"
595 );
596 }
597 other => panic!("expected MessageTooLarge, got {other:?}"),
598 }
599 }
600
601 #[tokio::test]
602 async fn ipc_accepts_message_at_limit_boundary() {
603 let bus = Arc::new(MessageBus::new());
606 let ipc = KernelIpc::new(bus.clone());
607
608 let msg = KernelMessage::new(
609 0,
610 MessageTarget::Process(1),
611 MessagePayload::Binary(vec![0u8; 1024]),
612 );
613
614 ipc.send(&msg).unwrap();
615 let received = bus.consume_inbound().await.unwrap();
616 assert_eq!(received.channel, "kernel-ipc");
617 }
618
619 #[test]
620 fn message_target_variants() {
621 let targets = vec![
622 MessageTarget::Process(1),
623 MessageTarget::Broadcast,
624 MessageTarget::Service("test".into()),
625 MessageTarget::ServiceMethod {
626 service: "auth".into(),
627 method: "validate_token".into(),
628 },
629 MessageTarget::Kernel,
630 ];
631 for target in targets {
632 let json = serde_json::to_string(&target).unwrap();
633 let _: MessageTarget = serde_json::from_str(&json).unwrap();
634 }
635 }
636
637 #[test]
638 fn kernel_signal_variants() {
639 let signals = vec![
640 KernelSignal::Shutdown,
641 KernelSignal::Suspend,
642 KernelSignal::Resume,
643 KernelSignal::Ping,
644 KernelSignal::Pong,
645 ];
646 for signal in signals {
647 let json = serde_json::to_string(&signal).unwrap();
648 let _: KernelSignal = serde_json::from_str(&json).unwrap();
649 }
650 }
651
652 #[test]
653 fn expanded_signal_variants_serde() {
654 let signals = vec![
655 KernelSignal::ReloadConfig,
656 KernelSignal::DumpState,
657 KernelSignal::UserDefined(42),
658 KernelSignal::Kill,
659 KernelSignal::LinkExit {
660 pid: 7,
661 reason: ExitReason::Crash("boom".into()),
662 },
663 KernelSignal::MonitorDown(ProcessDown {
664 pid: 9,
665 reason: ExitReason::Normal,
666 }),
667 KernelSignal::ResourceWarning {
668 resource: "memory".into(),
669 current: 800,
670 limit: 1000,
671 },
672 ];
673 for signal in signals {
674 let json = serde_json::to_string(&signal).unwrap();
675 let restored: KernelSignal = serde_json::from_str(&json).unwrap();
676 let json2 = serde_json::to_string(&restored).unwrap();
678 assert_eq!(json, json2);
679 }
680 }
681
682 #[test]
683 fn exit_reason_variants() {
684 let reasons = vec![
685 ExitReason::Normal,
686 ExitReason::Crash("error".into()),
687 ExitReason::Killed,
688 ExitReason::Timeout,
689 ];
690 for reason in reasons {
691 let json = serde_json::to_string(&reason).unwrap();
692 let _: ExitReason = serde_json::from_str(&json).unwrap();
693 }
694 }
695
696 #[test]
697 fn process_down_serde() {
698 let pd = ProcessDown {
699 pid: 42,
700 reason: ExitReason::Crash("segfault".into()),
701 };
702 let json = serde_json::to_string(&pd).unwrap();
703 let restored: ProcessDown = serde_json::from_str(&json).unwrap();
704 assert_eq!(restored.pid, 42);
705 assert!(matches!(restored.reason, ExitReason::Crash(ref s) if s == "segfault"));
706 }
707
708 #[test]
709 fn message_with_correlation_id() {
710 let msg = KernelMessage::with_correlation(
711 1,
712 MessageTarget::Process(2),
713 MessagePayload::Text("request".into()),
714 "req-123".into(),
715 );
716 assert_eq!(msg.correlation_id, Some("req-123".into()));
717 assert_eq!(msg.from, 1);
718 }
719
720 #[test]
721 fn message_without_correlation_id() {
722 let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
723 assert!(msg.correlation_id.is_none());
724 }
725
726 #[test]
727 fn tool_call_message() {
728 let msg = KernelMessage::tool_call(
729 1,
730 MessageTarget::Process(2),
731 "read_file",
732 serde_json::json!({"path": "/src/main.rs"}),
733 );
734 match &msg.payload {
735 MessagePayload::ToolCall { name, args } => {
736 assert_eq!(name, "read_file");
737 assert_eq!(args["path"], "/src/main.rs");
738 }
739 other => panic!("expected ToolCall, got: {other:?}"),
740 }
741 }
742
743 #[test]
744 fn tool_result_message() {
745 let msg = KernelMessage::tool_result(
746 2,
747 MessageTarget::Process(1),
748 "call-123",
749 serde_json::json!({"content": "file contents"}),
750 );
751 match &msg.payload {
752 MessagePayload::ToolResult { call_id, result } => {
753 assert_eq!(call_id, "call-123");
754 assert_eq!(result["content"], "file contents");
755 }
756 other => panic!("expected ToolResult, got: {other:?}"),
757 }
758 }
759
760 #[test]
761 fn topic_target() {
762 let msg = KernelMessage::text(1, MessageTarget::Topic("build-status".into()), "done");
763 assert!(matches!(msg.target, MessageTarget::Topic(ref t) if t == "build-status"));
764 }
765
766 #[test]
767 fn tool_call_serde_roundtrip() {
768 let msg = KernelMessage::tool_call(
769 1,
770 MessageTarget::Process(2),
771 "search",
772 serde_json::json!({"query": "test"}),
773 );
774 let json = serde_json::to_string(&msg).unwrap();
775 let restored: KernelMessage = serde_json::from_str(&json).unwrap();
776 assert!(matches!(
777 restored.payload,
778 MessagePayload::ToolCall { ref name, .. } if name == "search"
779 ));
780 }
781
782 #[test]
783 fn correlation_id_serde_roundtrip() {
784 let msg = KernelMessage::with_correlation(
785 1,
786 MessageTarget::Process(2),
787 MessagePayload::Text("req".into()),
788 "corr-456".into(),
789 );
790 let json = serde_json::to_string(&msg).unwrap();
791 let restored: KernelMessage = serde_json::from_str(&json).unwrap();
792 assert_eq!(restored.correlation_id, Some("corr-456".into()));
793 }
794
795 #[test]
796 fn rvf_payload_variant() {
797 let payload = MessagePayload::Rvf {
798 segment_type: 0x40,
799 data: vec![0xCA, 0xFE],
800 };
801 let msg = KernelMessage::new(1, MessageTarget::Process(2), payload);
802 assert_eq!(msg.payload.type_name(), "rvf");
803
804 let json = serde_json::to_string(&msg).unwrap();
806 let restored: KernelMessage = serde_json::from_str(&json).unwrap();
807 match &restored.payload {
808 MessagePayload::Rvf { segment_type, data } => {
809 assert_eq!(*segment_type, 0x40);
810 assert_eq!(data, &[0xCA, 0xFE]);
811 }
812 other => panic!("expected Rvf, got: {other:?}"),
813 }
814 }
815
816 #[test]
817 fn payload_type_names() {
818 assert_eq!(MessagePayload::Text("hi".into()).type_name(), "text");
819 assert_eq!(
820 MessagePayload::Json(serde_json::json!(1)).type_name(),
821 "json"
822 );
823 assert_eq!(
824 MessagePayload::Signal(KernelSignal::Ping).type_name(),
825 "signal"
826 );
827 assert_eq!(
828 MessagePayload::Binary(vec![0xDE, 0xAD]).type_name(),
829 "binary"
830 );
831 }
832
833 #[test]
834 fn binary_payload_serde_roundtrip() {
835 let payload = MessagePayload::Binary(vec![0x01, 0x02, 0x03, 0xFF]);
836 let json = serde_json::to_string(&payload).unwrap();
837 let restored: MessagePayload = serde_json::from_str(&json).unwrap();
838 match restored {
839 MessagePayload::Binary(data) => {
840 assert_eq!(data, vec![0x01, 0x02, 0x03, 0xFF]);
841 }
842 other => panic!("expected Binary, got {:?}", other),
843 }
844 }
845
846 #[test]
847 fn remote_node_serde_roundtrip() {
848 let target = MessageTarget::RemoteNode {
849 node_id: "node-42".into(),
850 target: Box::new(MessageTarget::Process(7)),
851 };
852 let json = serde_json::to_string(&target).unwrap();
853 let restored: MessageTarget = serde_json::from_str(&json).unwrap();
854 match restored {
855 MessageTarget::RemoteNode { node_id, target } => {
856 assert_eq!(node_id, "node-42");
857 assert!(matches!(*target, MessageTarget::Process(7)));
858 }
859 other => panic!("expected RemoteNode, got: {other:?}"),
860 }
861 }
862
863 #[test]
864 fn global_pid_equality() {
865 let a = GlobalPid::local(1, "node-a");
866 let b = GlobalPid::local(1, "node-b");
867 let c = GlobalPid::local(1, "node-a");
868 assert_ne!(a, b, "same pid on different nodes must not be equal");
869 assert_eq!(a, c, "same pid on same node must be equal");
870 }
871
872 #[test]
873 fn global_pid_is_local() {
874 let gpid = GlobalPid::local(5, "my-node");
875 assert!(gpid.is_local("my-node"));
876 assert!(!gpid.is_local("other-node"));
877 }
878
879 #[test]
880 fn global_pid_display() {
881 let gpid = GlobalPid::local(42, "alpha");
882 assert_eq!(gpid.to_string(), "alpha:42");
883 }
884
885 #[test]
886 fn global_pid_as_remote_target_and_composite_alias() {
887 let gpid = GlobalPid::new("node-x", 7);
888 let target = gpid.as_remote_target();
889 match target {
890 MessageTarget::RemoteNode { node_id, target } => {
891 assert_eq!(node_id, "node-x");
892 assert!(matches!(*target, MessageTarget::Process(7)));
893 }
894 other => panic!("expected RemoteNode, got {other:?}"),
895 }
896 let alias: CompositePid = gpid.clone();
897 assert_eq!(alias.pid, 7);
898 assert_eq!(alias.node_id, "node-x");
899 }
900
901 #[test]
902 fn correlation_id_absent_in_json_when_none() {
903 let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
904 let json = serde_json::to_string(&msg).unwrap();
905 assert!(!json.contains("correlation_id"));
906 }
907
908 #[test]
911 fn trace_id_absent_by_default() {
912 let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
913 assert!(msg.trace_id.is_none());
914 }
915
916 #[test]
917 fn trace_id_with_trace() {
918 let msg = KernelMessage::with_trace(
919 1,
920 MessageTarget::Process(2),
921 MessagePayload::Text("traced".into()),
922 "trace-abc-123".into(),
923 );
924 assert_eq!(msg.trace_id, Some("trace-abc-123".into()));
925 }
926
927 #[test]
928 fn trace_id_set_builder() {
929 let msg =
930 KernelMessage::text(1, MessageTarget::Process(2), "hello").set_trace_id("my-trace");
931 assert_eq!(msg.trace_id, Some("my-trace".into()));
932 }
933
934 #[test]
935 fn trace_id_ensure_generates() {
936 let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello").ensure_trace_id();
937 assert!(msg.trace_id.is_some());
938 assert!(!msg.trace_id.as_ref().unwrap().is_empty());
939 }
940
941 #[test]
942 fn trace_id_ensure_preserves_existing() {
943 let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello")
944 .set_trace_id("existing-trace")
945 .ensure_trace_id();
946 assert_eq!(msg.trace_id, Some("existing-trace".into()));
947 }
948
949 #[test]
950 fn trace_id_serde_roundtrip() {
951 let msg = KernelMessage::with_trace(
952 1,
953 MessageTarget::Process(2),
954 MessagePayload::Text("traced".into()),
955 "trace-roundtrip".into(),
956 );
957 let json = serde_json::to_string(&msg).unwrap();
958 let restored: KernelMessage = serde_json::from_str(&json).unwrap();
959 assert_eq!(restored.trace_id, Some("trace-roundtrip".into()));
960 }
961
962 #[test]
963 fn trace_id_absent_in_json_when_none() {
964 let msg = KernelMessage::text(1, MessageTarget::Process(2), "hello");
965 let json = serde_json::to_string(&msg).unwrap();
966 assert!(!json.contains("trace_id"));
967 }
968
969 #[test]
970 fn trace_id_backward_compat_deserialization() {
971 let json = r#"{"id":"test-id","from":1,"target":{"Process":2},"payload":{"Text":"hello"},"timestamp":"2024-01-01T00:00:00Z"}"#;
973 let msg: KernelMessage = serde_json::from_str(json).unwrap();
974 assert!(msg.trace_id.is_none());
975 assert!(msg.correlation_id.is_none());
976 }
977}