//! TG3: Channel Message Identity & Routing Tests //! //! Prevents: Pattern 3 — Channel message routing & identity bugs (17% of user bugs). //! Issues: #496, #483, #620, #415, #503 //! //! Tests that ChannelMessage fields are used consistently and that the //! SendMessage → Channel trait contract preserves correct identity semantics. //! Verifies sender/reply_target field contracts to prevent field swaps. use async_trait::async_trait; use zeroclaw::channels::traits::{Channel, ChannelMessage, SendMessage}; // ───────────────────────────────────────────────────────────────────────────── // ChannelMessage construction and field semantics // ───────────────────────────────────────────────────────────────────────────── #[test] fn channel_message_sender_field_holds_platform_user_id() { // Simulates Telegram: sender should be numeric chat_id, not username let msg = ChannelMessage { id: "msg_1".into(), sender: "123456789".into(), // numeric chat_id reply_target: "msg_0".into(), content: "test message".into(), channel: "telegram".into(), timestamp: 1700000000, }; assert_eq!(msg.sender, "123456789"); // Sender should be the platform-level user/chat identifier assert!( msg.sender.chars().all(|c| c.is_ascii_digit()), "Telegram sender should be numeric chat_id, got: {}", msg.sender ); } #[test] fn channel_message_reply_target_distinct_from_sender() { // Simulates Discord: reply_target should be channel_id, not sender user_id let msg = ChannelMessage { id: "msg_1".into(), sender: "user_987654".into(), // Discord user ID reply_target: "channel_123".into(), // Discord channel ID for replies content: "test message".into(), channel: "discord".into(), timestamp: 1700000000, }; assert_ne!( msg.sender, msg.reply_target, "sender and reply_target should be distinct for Discord" ); assert_eq!(msg.reply_target, "channel_123"); } #[test] fn channel_message_fields_not_swapped() { // Guards against #496 (Telegram) and #483 (Discord) field swap bugs let msg = ChannelMessage { id: "msg_42".into(), sender: "sender_value".into(), reply_target: "target_value".into(), content: "payload".into(), channel: "test".into(), timestamp: 1700000000, }; assert_eq!(msg.sender, "sender_value", "sender field should not be swapped"); assert_eq!( msg.reply_target, "target_value", "reply_target field should not be swapped" ); assert_ne!( msg.sender, msg.reply_target, "sender and reply_target should remain distinct" ); } #[test] fn channel_message_preserves_all_fields_on_clone() { let original = ChannelMessage { id: "clone_test".into(), sender: "sender_123".into(), reply_target: "target_456".into(), content: "cloned content".into(), channel: "test_channel".into(), timestamp: 1700000001, }; let cloned = original.clone(); assert_eq!(cloned.id, original.id); assert_eq!(cloned.sender, original.sender); assert_eq!(cloned.reply_target, original.reply_target); assert_eq!(cloned.content, original.content); assert_eq!(cloned.channel, original.channel); assert_eq!(cloned.timestamp, original.timestamp); } // ───────────────────────────────────────────────────────────────────────────── // SendMessage construction // ───────────────────────────────────────────────────────────────────────────── #[test] fn send_message_new_sets_content_and_recipient() { let msg = SendMessage::new("Hello", "recipient_123"); assert_eq!(msg.content, "Hello"); assert_eq!(msg.recipient, "recipient_123"); assert!(msg.subject.is_none(), "subject should be None by default"); } #[test] fn send_message_with_subject_sets_all_fields() { let msg = SendMessage::with_subject("Hello", "recipient_123", "Re: Test"); assert_eq!(msg.content, "Hello"); assert_eq!(msg.recipient, "recipient_123"); assert_eq!(msg.subject.as_deref(), Some("Re: Test")); } #[test] fn send_message_recipient_carries_platform_target() { // Verifies that SendMessage::recipient is used as the platform delivery target // For Telegram: this should be the chat_id // For Discord: this should be the channel_id let telegram_msg = SendMessage::new("response", "123456789"); assert_eq!( telegram_msg.recipient, "123456789", "Telegram SendMessage recipient should be chat_id" ); let discord_msg = SendMessage::new("response", "channel_987654"); assert_eq!( discord_msg.recipient, "channel_987654", "Discord SendMessage recipient should be channel_id" ); } // ───────────────────────────────────────────────────────────────────────────── // Channel trait contract: send/listen roundtrip via DummyChannel // ───────────────────────────────────────────────────────────────────────────── /// Test channel that captures sent messages for assertion struct CapturingChannel { sent: std::sync::Mutex>, } impl CapturingChannel { fn new() -> Self { Self { sent: std::sync::Mutex::new(Vec::new()), } } fn sent_messages(&self) -> Vec { self.sent.lock().unwrap().clone() } } #[async_trait] impl Channel for CapturingChannel { fn name(&self) -> &str { "capturing" } async fn send(&self, message: &SendMessage) -> anyhow::Result<()> { self.sent.lock().unwrap().push(message.clone()); Ok(()) } async fn listen( &self, tx: tokio::sync::mpsc::Sender, ) -> anyhow::Result<()> { tx.send(ChannelMessage { id: "listen_1".into(), sender: "test_sender".into(), reply_target: "test_target".into(), content: "incoming".into(), channel: "capturing".into(), timestamp: 1700000000, }) .await .map_err(|e| anyhow::anyhow!(e.to_string())) } } #[tokio::test] async fn channel_send_preserves_recipient() { let channel = CapturingChannel::new(); let msg = SendMessage::new("Hello", "target_123"); channel.send(&msg).await.unwrap(); let sent = channel.sent_messages(); assert_eq!(sent.len(), 1); assert_eq!(sent[0].recipient, "target_123"); assert_eq!(sent[0].content, "Hello"); } #[tokio::test] async fn channel_listen_produces_correct_identity_fields() { let channel = CapturingChannel::new(); let (tx, mut rx) = tokio::sync::mpsc::channel(1); channel.listen(tx).await.unwrap(); let received = rx.recv().await.expect("should receive message"); assert_eq!(received.sender, "test_sender"); assert_eq!(received.reply_target, "test_target"); assert_ne!( received.sender, received.reply_target, "listen() should populate sender and reply_target distinctly" ); } #[tokio::test] async fn channel_send_reply_uses_sender_from_listen() { let channel = CapturingChannel::new(); let (tx, mut rx) = tokio::sync::mpsc::channel(1); // Simulate: listen() → receive message → send reply using sender channel.listen(tx).await.unwrap(); let incoming = rx.recv().await.expect("should receive message"); // Reply should go to the reply_target, not sender let reply = SendMessage::new("reply content", &incoming.reply_target); channel.send(&reply).await.unwrap(); let sent = channel.sent_messages(); assert_eq!(sent.len(), 1); assert_eq!( sent[0].recipient, "test_target", "reply should use reply_target as recipient" ); } // ───────────────────────────────────────────────────────────────────────────── // Channel trait default methods // ───────────────────────────────────────────────────────────────────────────── #[tokio::test] async fn channel_health_check_default_returns_true() { let channel = CapturingChannel::new(); assert!( channel.health_check().await, "default health_check should return true" ); } #[tokio::test] async fn channel_typing_defaults_succeed() { let channel = CapturingChannel::new(); assert!(channel.start_typing("target").await.is_ok()); assert!(channel.stop_typing("target").await.is_ok()); } #[tokio::test] async fn channel_draft_defaults() { let channel = CapturingChannel::new(); assert!(!channel.supports_draft_updates()); let draft_result = channel .send_draft(&SendMessage::new("draft", "target")) .await .unwrap(); assert!(draft_result.is_none(), "default send_draft should return None"); assert!(channel .update_draft("target", "msg_1", "updated") .await .is_ok()); assert!(channel .finalize_draft("target", "msg_1", "final") .await .is_ok()); } // ───────────────────────────────────────────────────────────────────────────── // Multiple messages: conversation context preservation // ───────────────────────────────────────────────────────────────────────────── #[tokio::test] async fn channel_multiple_sends_preserve_order_and_recipients() { let channel = CapturingChannel::new(); channel .send(&SendMessage::new("msg 1", "target_a")) .await .unwrap(); channel .send(&SendMessage::new("msg 2", "target_b")) .await .unwrap(); channel .send(&SendMessage::new("msg 3", "target_a")) .await .unwrap(); let sent = channel.sent_messages(); assert_eq!(sent.len(), 3); assert_eq!(sent[0].recipient, "target_a"); assert_eq!(sent[1].recipient, "target_b"); assert_eq!(sent[2].recipient, "target_a"); assert_eq!(sent[0].content, "msg 1"); assert_eq!(sent[1].content, "msg 2"); assert_eq!(sent[2].content, "msg 3"); }