capo-agent 0.8.0

Coding-agent library built on motosan-agent-loop. Composable, embeddable.
Documentation
#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]

//! Event/Action wire types — the single JSON line over stdio.
//! Implemented in Task 4.

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventName {
    SessionBeforeSwitch,
    BeforeUserMessage,
    Command,
}

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum Event {
    SessionBeforeSwitch {
        reason: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
    },
    BeforeUserMessage {
        text: String,
        #[serde(skip_serializing_if = "Vec::is_empty")]
        attachments: Vec<crate::user_message::Attachment>,
    },
    Command {
        name: String,
        args: String,
    },
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Action {
    Continue,
    Cancel {
        #[serde(default)]
        reason: Option<String>,
    },
    TransformText {
        text: String,
    },
    Reply {
        text: String,
    },
    Send {
        text: String,
        #[serde(default)]
        attachments: Vec<crate::user_message::Attachment>,
    },
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn event_session_before_switch_serializes_with_reason() {
        let ev = Event::SessionBeforeSwitch {
            reason: "new".into(),
            session_id: None,
        };
        let json = serde_json::to_string(&ev).expect("serialize");
        assert_eq!(json, r#"{"event":"session_before_switch","reason":"new"}"#);
    }

    #[test]
    fn event_session_before_switch_includes_session_id_when_set() {
        let ev = Event::SessionBeforeSwitch {
            reason: "load".into(),
            session_id: Some("01HK".into()),
        };
        let json = serde_json::to_string(&ev).expect("serialize");
        assert_eq!(
            json,
            r#"{"event":"session_before_switch","reason":"load","session_id":"01HK"}"#
        );
    }

    #[test]
    fn action_continue_deserializes() {
        let action: Action = serde_json::from_str(r#"{"action":"continue"}"#).expect("deserialize");
        assert_eq!(action, Action::Continue);
    }

    #[test]
    fn action_cancel_with_reason_deserializes() {
        let action: Action =
            serde_json::from_str(r#"{"action":"cancel","reason":"nope"}"#).expect("deserialize");
        assert_eq!(
            action,
            Action::Cancel {
                reason: Some("nope".into())
            }
        );
    }

    #[test]
    fn action_cancel_without_reason_deserializes() {
        let action: Action = serde_json::from_str(r#"{"action":"cancel"}"#).expect("deserialize");
        assert_eq!(action, Action::Cancel { reason: None });
    }

    #[test]
    fn action_unknown_tag_rejected() {
        let result: Result<Action, _> = serde_json::from_str(r#"{"action":"unknown"}"#);
        assert!(result.is_err(), "unknown action tag should fail");
    }

    #[test]
    fn event_before_user_message_serializes_with_text_only() {
        let ev = Event::BeforeUserMessage {
            text: "hi".into(),
            attachments: Vec::new(),
        };
        let json = serde_json::to_string(&ev).expect("serialize");
        assert_eq!(json, r#"{"event":"before_user_message","text":"hi"}"#);
    }

    #[test]
    fn event_before_user_message_includes_attachments_when_present() {
        let ev = Event::BeforeUserMessage {
            text: "look".into(),
            attachments: vec![crate::user_message::Attachment::Image {
                path: std::path::PathBuf::from("/tmp/x.png"),
            }],
        };
        let json = serde_json::to_string(&ev).expect("serialize");
        assert!(json.contains(r#""event":"before_user_message""#));
        assert!(json.contains(r#""text":"look""#));
        assert!(json.contains(r#""attachments":[{"type":"image","path":"/tmp/x.png"}]"#));
    }

    #[test]
    fn event_command_serializes_with_name_and_args() {
        let ev = Event::Command {
            name: "todo".into(),
            args: "add buy milk".into(),
        };
        let json = serde_json::to_string(&ev).expect("serialize");
        assert_eq!(
            json,
            r#"{"event":"command","name":"todo","args":"add buy milk"}"#
        );
    }

    #[test]
    fn action_transform_text_deserializes() {
        let action: Action =
            serde_json::from_str(r#"{"action":"transform_text","text":"new"}"#).expect("ok");
        assert_eq!(action, Action::TransformText { text: "new".into() });
    }

    #[test]
    fn action_reply_deserializes() {
        let action: Action = serde_json::from_str(r#"{"action":"reply","text":"yo"}"#).expect("ok");
        assert_eq!(action, Action::Reply { text: "yo".into() });
    }

    #[test]
    fn action_send_with_attachments_deserializes() {
        let action: Action = serde_json::from_str(
            r#"{"action":"send","text":"go","attachments":[{"type":"image","path":"/tmp/x.png"}]}"#,
        )
        .expect("ok");
        match action {
            Action::Send { text, attachments } => {
                assert_eq!(text, "go");
                assert_eq!(attachments.len(), 1);
            }
            other => panic!("expected Send; got {other:?}"),
        }
    }

    #[test]
    fn action_send_without_attachments_deserializes() {
        let action: Action = serde_json::from_str(r#"{"action":"send","text":"go"}"#).expect("ok");
        match action {
            Action::Send { text, attachments } => {
                assert_eq!(text, "go");
                assert!(attachments.is_empty());
            }
            other => panic!("expected Send; got {other:?}"),
        }
    }

    #[test]
    fn event_name_serializes_to_snake_case() {
        assert_eq!(
            serde_json::to_string(&EventName::SessionBeforeSwitch).expect("ser"),
            "\"session_before_switch\""
        );
        assert_eq!(
            serde_json::to_string(&EventName::BeforeUserMessage).expect("ser"),
            "\"before_user_message\""
        );
        assert_eq!(
            serde_json::to_string(&EventName::Command).expect("ser"),
            "\"command\""
        );
    }
}