gemini_genai_rs/protocol/messages/
mod.rs

1//! Client→Server and Server→Client message envelopes for the Gemini Live wire protocol.
2
3pub mod client;
4pub mod server;
5
6pub use client::*;
7pub use server::*;
8
9#[cfg(test)]
10mod tests {
11    use super::*;
12    use crate::protocol::types::*;
13
14    #[test]
15    fn setup_message_serialization() {
16        let config = SessionConfig::new("test-key")
17            .model(ModelId::from_static("models/gemini-2.0-flash-live-001"))
18            .voice(Voice::Kore)
19            .system_instruction("You are a helpful assistant.");
20
21        let json = config.to_setup_json();
22        assert!(json.contains("\"setup\""));
23        assert!(json.contains("\"generationConfig\""));
24        assert!(json.contains("\"Kore\""));
25        assert!(json.contains("\"systemInstruction\""));
26    }
27
28    #[test]
29    fn parse_setup_complete() {
30        let json = r#"{"setupComplete":{"sessionResumption":{"handle":"abc123"}}}"#;
31        let msg = ServerMessage::parse(json).unwrap();
32        match msg {
33            ServerMessage::SetupComplete(sc) => {
34                let handle = sc.setup_complete.session_resumption.unwrap().handle;
35                assert_eq!(handle, Some("abc123".to_string()));
36            }
37            _ => panic!("Expected SetupComplete"),
38        }
39    }
40
41    #[test]
42    fn parse_server_content_text() {
43        let json = r#"{
44            "serverContent": {
45                "modelTurn": {
46                    "parts": [{"text": "Hello! How can I help?"}]
47                },
48                "turnComplete": true
49            }
50        }"#;
51        let msg = ServerMessage::parse(json).unwrap();
52        match msg {
53            ServerMessage::ServerContent(sc) => {
54                assert!(sc.server_content.turn_complete.unwrap_or(false));
55                let turn = sc.server_content.model_turn.unwrap();
56                assert_eq!(turn.parts.len(), 1);
57                match &turn.parts[0] {
58                    Part::Text { text } => assert_eq!(text, "Hello! How can I help?"),
59                    _ => panic!("Expected text part"),
60                }
61            }
62            _ => panic!("Expected ServerContent"),
63        }
64    }
65
66    #[test]
67    fn parse_server_content_audio() {
68        let json = r#"{
69            "serverContent": {
70                "modelTurn": {
71                    "parts": [{"inlineData": {"mimeType": "audio/pcm", "data": "AAAA"}}]
72                }
73            }
74        }"#;
75        let msg = ServerMessage::parse(json).unwrap();
76        match msg {
77            ServerMessage::ServerContent(sc) => {
78                let turn = sc.server_content.model_turn.unwrap();
79                match &turn.parts[0] {
80                    Part::InlineData { inline_data } => {
81                        assert_eq!(inline_data.mime_type, "audio/pcm");
82                    }
83                    _ => panic!("Expected inline data part"),
84                }
85            }
86            _ => panic!("Expected ServerContent"),
87        }
88    }
89
90    #[test]
91    fn parse_tool_call() {
92        let json = r#"{
93            "toolCall": {
94                "functionCalls": [
95                    {"name": "get_weather", "args": {"city": "London"}, "id": "call-1"}
96                ]
97            }
98        }"#;
99        let msg = ServerMessage::parse(json).unwrap();
100        match msg {
101            ServerMessage::ToolCall(tc) => {
102                assert_eq!(tc.tool_call.function_calls.len(), 1);
103                assert_eq!(tc.tool_call.function_calls[0].name, "get_weather");
104            }
105            _ => panic!("Expected ToolCall"),
106        }
107    }
108
109    #[test]
110    fn parse_tool_call_cancellation() {
111        let json = r#"{"toolCallCancellation": {"ids": ["call-1", "call-2"]}}"#;
112        let msg = ServerMessage::parse(json).unwrap();
113        match msg {
114            ServerMessage::ToolCallCancellation(tc) => {
115                assert_eq!(tc.tool_call_cancellation.ids, vec!["call-1", "call-2"]);
116            }
117            _ => panic!("Expected ToolCallCancellation"),
118        }
119    }
120
121    #[test]
122    fn parse_go_away() {
123        let json = r#"{"goAway": {"timeLeft": "30s"}}"#;
124        let msg = ServerMessage::parse(json).unwrap();
125        match msg {
126            ServerMessage::GoAway(ga) => {
127                assert_eq!(
128                    ga.go_away.time_left,
129                    Some(std::time::Duration::from_secs(30))
130                );
131            }
132            _ => panic!("Expected GoAway"),
133        }
134    }
135
136    #[test]
137    fn parse_interrupted() {
138        let json = r#"{"serverContent": {"interrupted": true}}"#;
139        let msg = ServerMessage::parse(json).unwrap();
140        match msg {
141            ServerMessage::ServerContent(sc) => {
142                assert!(sc.server_content.interrupted.unwrap_or(false));
143            }
144            _ => panic!("Expected ServerContent"),
145        }
146    }
147
148    #[test]
149    fn parse_unknown_message() {
150        let json = r#"{"newFeature": {"value": 42}}"#;
151        let msg = ServerMessage::parse(json).unwrap();
152        assert!(matches!(msg, ServerMessage::Unknown(_)));
153    }
154
155    #[test]
156    fn realtime_input_serialization_audio() {
157        let msg = RealtimeInputMessage {
158            realtime_input: RealtimeInputPayload {
159                media_chunks: Vec::new(),
160                audio: Some(Blob {
161                    mime_type: "audio/pcm".to_string(),
162                    data: "AQIDBA==".to_string(),
163                }),
164                video: None,
165                audio_stream_end: None,
166                text: None,
167            },
168        };
169        let json = serde_json::to_string(&msg).unwrap();
170        assert!(json.contains("\"realtimeInput\""));
171        assert!(json.contains("\"audio\""));
172        assert!(json.contains("\"mimeType\""));
173        // Deprecated field should not appear when empty
174        assert!(!json.contains("\"mediaChunks\""));
175    }
176
177    #[test]
178    fn realtime_input_serialization_legacy() {
179        let msg = RealtimeInputMessage {
180            realtime_input: RealtimeInputPayload {
181                media_chunks: vec![MediaChunk {
182                    mime_type: "audio/pcm".to_string(),
183                    data: "AQIDBA==".to_string(),
184                }],
185                audio: None,
186                video: None,
187                audio_stream_end: None,
188                text: None,
189            },
190        };
191        let json = serde_json::to_string(&msg).unwrap();
192        assert!(json.contains("\"mediaChunks\""));
193    }
194
195    #[test]
196    fn parse_session_resumption_update() {
197        let json = r#"{"sessionResumptionUpdate": {"newHandle": "handle-xyz", "resumable": true}}"#;
198        let msg = ServerMessage::parse(json).unwrap();
199        match msg {
200            ServerMessage::SessionResumptionUpdate(sru) => {
201                assert_eq!(
202                    sru.session_resumption_update.new_handle,
203                    Some("handle-xyz".to_string())
204                );
205                assert_eq!(sru.session_resumption_update.resumable, Some(true));
206            }
207            _ => panic!("Expected SessionResumptionUpdate"),
208        }
209    }
210
211    #[test]
212    fn tool_response_serialization() {
213        let msg = ToolResponseMessage {
214            tool_response: ToolResponsePayload {
215                function_responses: vec![FunctionResponse {
216                    name: "get_weather".to_string(),
217                    response: serde_json::json!({"temp": 22}),
218                    id: Some("call-1".to_string()),
219                    scheduling: None,
220                }],
221            },
222        };
223        let json = serde_json::to_string(&msg).unwrap();
224        assert!(json.contains("\"toolResponse\""));
225        assert!(json.contains("\"functionResponses\""));
226    }
227
228    #[test]
229    fn client_content_serialization() {
230        let msg = ClientContentMessage {
231            client_content: ClientContentPayload {
232                turns: vec![Content::user("Hello")],
233                turn_complete: Some(true),
234            },
235        };
236        let json = serde_json::to_string(&msg).unwrap();
237        assert!(json.contains("\"clientContent\""));
238        assert!(json.contains("\"turnComplete\""));
239    }
240
241    #[test]
242    fn activity_signal_serialization() {
243        let msg = ActivitySignalMessage {
244            realtime_input: ActivitySignalPayload {
245                activity_start: Some(ActivityStart {}),
246                activity_end: None,
247            },
248        };
249        let json = serde_json::to_string(&msg).unwrap();
250        assert!(json.contains("\"activityStart\""));
251    }
252
253    #[test]
254    fn voice_activity_type_serialization() {
255        let start = VoiceActivityType::VoiceActivityStart;
256        let json = serde_json::to_string(&start).unwrap();
257        assert_eq!(json, "\"VOICE_ACTIVITY_START\"");
258        let parsed: VoiceActivityType = serde_json::from_str(&json).unwrap();
259        assert_eq!(parsed, start);
260
261        let end = VoiceActivityType::VoiceActivityEnd;
262        let json = serde_json::to_string(&end).unwrap();
263        assert_eq!(json, "\"VOICE_ACTIVITY_END\"");
264        let parsed: VoiceActivityType = serde_json::from_str(&json).unwrap();
265        assert_eq!(parsed, end);
266    }
267
268    #[test]
269    fn parse_voice_activity_message() {
270        let json = r#"{"voiceActivity":{"voiceActivityType":"VOICE_ACTIVITY_START"}}"#;
271        let msg = ServerMessage::parse(json).unwrap();
272        match msg {
273            ServerMessage::VoiceActivity(va) => {
274                assert_eq!(
275                    va.voice_activity.voice_activity_type,
276                    Some(VoiceActivityType::VoiceActivityStart)
277                );
278            }
279            _ => panic!("Expected VoiceActivity"),
280        }
281
282        let json = r#"{"voiceActivity":{"voiceActivityType":"VOICE_ACTIVITY_END"}}"#;
283        let msg = ServerMessage::parse(json).unwrap();
284        match msg {
285            ServerMessage::VoiceActivity(va) => {
286                assert_eq!(
287                    va.voice_activity.voice_activity_type,
288                    Some(VoiceActivityType::VoiceActivityEnd)
289                );
290            }
291            _ => panic!("Expected VoiceActivity"),
292        }
293    }
294
295    #[test]
296    fn parse_input_transcription() {
297        let json = r#"{
298            "serverContent": {
299                "inputTranscription": {"text": "Hello world"}
300            }
301        }"#;
302        let msg = ServerMessage::parse(json).unwrap();
303        match msg {
304            ServerMessage::ServerContent(sc) => {
305                let text = sc.server_content.input_transcription.unwrap().text.unwrap();
306                assert_eq!(text, "Hello world");
307            }
308            _ => panic!("Expected ServerContent"),
309        }
310    }
311}