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