Skip to main content

construct/channels/
cli.rs

1use super::traits::{Channel, ChannelMessage, SendMessage};
2use async_trait::async_trait;
3use tokio::io::{self, AsyncBufReadExt, BufReader};
4use uuid::Uuid;
5
6/// CLI channel — stdin/stdout, always available, zero deps
7pub struct CliChannel;
8
9impl CliChannel {
10    pub fn new() -> Self {
11        Self
12    }
13}
14
15#[async_trait]
16impl Channel for CliChannel {
17    fn name(&self) -> &str {
18        "cli"
19    }
20
21    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
22        println!("{}", message.content);
23        Ok(())
24    }
25
26    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
27        let stdin = io::stdin();
28        let reader = BufReader::new(stdin);
29        let mut lines = reader.lines();
30
31        while let Ok(Some(line)) = lines.next_line().await {
32            let line = line.trim().to_string();
33            if line.is_empty() {
34                continue;
35            }
36            if line == "/quit" || line == "/exit" {
37                break;
38            }
39
40            let msg = ChannelMessage {
41                id: Uuid::new_v4().to_string(),
42                sender: "user".to_string(),
43                reply_target: "user".to_string(),
44                content: line,
45                channel: "cli".to_string(),
46                timestamp: std::time::SystemTime::now()
47                    .duration_since(std::time::UNIX_EPOCH)
48                    .unwrap_or_default()
49                    .as_secs(),
50                thread_ts: None,
51                interruption_scope_id: None,
52                attachments: vec![],
53            };
54
55            if tx.send(msg).await.is_err() {
56                break;
57            }
58        }
59        Ok(())
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn cli_channel_name() {
69        assert_eq!(CliChannel::new().name(), "cli");
70    }
71
72    #[tokio::test]
73    async fn cli_channel_send_does_not_panic() {
74        let ch = CliChannel::new();
75        let result = ch
76            .send(&SendMessage {
77                content: "hello".into(),
78                recipient: "user".into(),
79                subject: None,
80                thread_ts: None,
81                cancellation_token: None,
82            })
83            .await;
84        assert!(result.is_ok());
85    }
86
87    #[tokio::test]
88    async fn cli_channel_send_empty_message() {
89        let ch = CliChannel::new();
90        let result = ch
91            .send(&SendMessage {
92                content: String::new(),
93                recipient: String::new(),
94                subject: None,
95                thread_ts: None,
96                cancellation_token: None,
97            })
98            .await;
99        assert!(result.is_ok());
100    }
101
102    #[tokio::test]
103    async fn cli_channel_health_check() {
104        let ch = CliChannel::new();
105        assert!(ch.health_check().await);
106    }
107
108    #[test]
109    fn channel_message_struct() {
110        let msg = ChannelMessage {
111            id: "test-id".into(),
112            sender: "user".into(),
113            reply_target: "user".into(),
114            content: "hello".into(),
115            channel: "cli".into(),
116            timestamp: 1_234_567_890,
117            thread_ts: None,
118            interruption_scope_id: None,
119            attachments: vec![],
120        };
121        assert_eq!(msg.id, "test-id");
122        assert_eq!(msg.sender, "user");
123        assert_eq!(msg.reply_target, "user");
124        assert_eq!(msg.content, "hello");
125        assert_eq!(msg.channel, "cli");
126        assert_eq!(msg.timestamp, 1_234_567_890);
127    }
128
129    #[test]
130    fn channel_message_clone() {
131        let msg = ChannelMessage {
132            id: "id".into(),
133            sender: "s".into(),
134            reply_target: "s".into(),
135            content: "c".into(),
136            channel: "ch".into(),
137            timestamp: 0,
138            thread_ts: None,
139            interruption_scope_id: None,
140            attachments: vec![],
141        };
142        let cloned = msg.clone();
143        assert_eq!(cloned.id, msg.id);
144        assert_eq!(cloned.content, msg.content);
145    }
146}