Skip to main content

capo_agent/extensions/
wire.rs

1#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
2
3//! Event/Action wire types — the single JSON line over stdio.
4//! Implemented in Task 4.
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum EventName {
11    SessionBeforeSwitch,
12    BeforeUserMessage,
13    Command,
14}
15
16#[derive(Debug, Clone, Serialize)]
17#[serde(tag = "event", rename_all = "snake_case")]
18pub enum Event {
19    SessionBeforeSwitch {
20        reason: String,
21        #[serde(skip_serializing_if = "Option::is_none")]
22        session_id: Option<String>,
23    },
24    BeforeUserMessage {
25        text: String,
26        #[serde(skip_serializing_if = "Vec::is_empty")]
27        attachments: Vec<crate::user_message::Attachment>,
28    },
29    Command {
30        name: String,
31        args: String,
32    },
33}
34
35#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
36#[serde(tag = "action", rename_all = "snake_case")]
37pub enum Action {
38    Continue,
39    Cancel {
40        #[serde(default)]
41        reason: Option<String>,
42    },
43    TransformText {
44        text: String,
45    },
46    Reply {
47        text: String,
48    },
49    Send {
50        text: String,
51        #[serde(default)]
52        attachments: Vec<crate::user_message::Attachment>,
53    },
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use pretty_assertions::assert_eq;
60
61    #[test]
62    fn event_session_before_switch_serializes_with_reason() {
63        let ev = Event::SessionBeforeSwitch {
64            reason: "new".into(),
65            session_id: None,
66        };
67        let json = serde_json::to_string(&ev).expect("serialize");
68        assert_eq!(json, r#"{"event":"session_before_switch","reason":"new"}"#);
69    }
70
71    #[test]
72    fn event_session_before_switch_includes_session_id_when_set() {
73        let ev = Event::SessionBeforeSwitch {
74            reason: "load".into(),
75            session_id: Some("01HK".into()),
76        };
77        let json = serde_json::to_string(&ev).expect("serialize");
78        assert_eq!(
79            json,
80            r#"{"event":"session_before_switch","reason":"load","session_id":"01HK"}"#
81        );
82    }
83
84    #[test]
85    fn action_continue_deserializes() {
86        let action: Action = serde_json::from_str(r#"{"action":"continue"}"#).expect("deserialize");
87        assert_eq!(action, Action::Continue);
88    }
89
90    #[test]
91    fn action_cancel_with_reason_deserializes() {
92        let action: Action =
93            serde_json::from_str(r#"{"action":"cancel","reason":"nope"}"#).expect("deserialize");
94        assert_eq!(
95            action,
96            Action::Cancel {
97                reason: Some("nope".into())
98            }
99        );
100    }
101
102    #[test]
103    fn action_cancel_without_reason_deserializes() {
104        let action: Action = serde_json::from_str(r#"{"action":"cancel"}"#).expect("deserialize");
105        assert_eq!(action, Action::Cancel { reason: None });
106    }
107
108    #[test]
109    fn action_unknown_tag_rejected() {
110        let result: Result<Action, _> = serde_json::from_str(r#"{"action":"unknown"}"#);
111        assert!(result.is_err(), "unknown action tag should fail");
112    }
113
114    #[test]
115    fn event_before_user_message_serializes_with_text_only() {
116        let ev = Event::BeforeUserMessage {
117            text: "hi".into(),
118            attachments: Vec::new(),
119        };
120        let json = serde_json::to_string(&ev).expect("serialize");
121        assert_eq!(json, r#"{"event":"before_user_message","text":"hi"}"#);
122    }
123
124    #[test]
125    fn event_before_user_message_includes_attachments_when_present() {
126        let ev = Event::BeforeUserMessage {
127            text: "look".into(),
128            attachments: vec![crate::user_message::Attachment::Image {
129                path: std::path::PathBuf::from("/tmp/x.png"),
130            }],
131        };
132        let json = serde_json::to_string(&ev).expect("serialize");
133        assert!(json.contains(r#""event":"before_user_message""#));
134        assert!(json.contains(r#""text":"look""#));
135        assert!(json.contains(r#""attachments":[{"type":"image","path":"/tmp/x.png"}]"#));
136    }
137
138    #[test]
139    fn event_command_serializes_with_name_and_args() {
140        let ev = Event::Command {
141            name: "todo".into(),
142            args: "add buy milk".into(),
143        };
144        let json = serde_json::to_string(&ev).expect("serialize");
145        assert_eq!(
146            json,
147            r#"{"event":"command","name":"todo","args":"add buy milk"}"#
148        );
149    }
150
151    #[test]
152    fn action_transform_text_deserializes() {
153        let action: Action =
154            serde_json::from_str(r#"{"action":"transform_text","text":"new"}"#).expect("ok");
155        assert_eq!(action, Action::TransformText { text: "new".into() });
156    }
157
158    #[test]
159    fn action_reply_deserializes() {
160        let action: Action = serde_json::from_str(r#"{"action":"reply","text":"yo"}"#).expect("ok");
161        assert_eq!(action, Action::Reply { text: "yo".into() });
162    }
163
164    #[test]
165    fn action_send_with_attachments_deserializes() {
166        let action: Action = serde_json::from_str(
167            r#"{"action":"send","text":"go","attachments":[{"type":"image","path":"/tmp/x.png"}]}"#,
168        )
169        .expect("ok");
170        match action {
171            Action::Send { text, attachments } => {
172                assert_eq!(text, "go");
173                assert_eq!(attachments.len(), 1);
174            }
175            other => panic!("expected Send; got {other:?}"),
176        }
177    }
178
179    #[test]
180    fn action_send_without_attachments_deserializes() {
181        let action: Action = serde_json::from_str(r#"{"action":"send","text":"go"}"#).expect("ok");
182        match action {
183            Action::Send { text, attachments } => {
184                assert_eq!(text, "go");
185                assert!(attachments.is_empty());
186            }
187            other => panic!("expected Send; got {other:?}"),
188        }
189    }
190
191    #[test]
192    fn event_name_serializes_to_snake_case() {
193        assert_eq!(
194            serde_json::to_string(&EventName::SessionBeforeSwitch).expect("ser"),
195            "\"session_before_switch\""
196        );
197        assert_eq!(
198            serde_json::to_string(&EventName::BeforeUserMessage).expect("ser"),
199            "\"before_user_message\""
200        );
201        assert_eq!(
202            serde_json::to_string(&EventName::Command).expect("ser"),
203            "\"command\""
204        );
205    }
206}