Skip to main content

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_usage_only_message() {
150        let json = r#"{"usageMetadata": {"promptTokenCount": 12, "totalTokenCount": 30}}"#;
151        let msg = ServerMessage::parse(json).unwrap();
152        match msg {
153            ServerMessage::ServerContent(sc) => {
154                let usage = sc.usage_metadata.expect("usage kept");
155                assert_eq!(usage.prompt_token_count, Some(12));
156                assert_eq!(usage.total_token_count, Some(30));
157                assert!(sc.server_content.model_turn.is_none());
158                assert!(sc.server_content.turn_complete.is_none());
159            }
160            other => panic!("Expected ServerContent, got {other:?}"),
161        }
162    }
163
164    #[test]
165    fn parse_unknown_message() {
166        let json = r#"{"newFeature": {"value": 42}}"#;
167        let msg = ServerMessage::parse(json).unwrap();
168        assert!(matches!(msg, ServerMessage::Unknown(_)));
169    }
170
171    #[test]
172    fn realtime_input_serialization_audio() {
173        let msg = RealtimeInputMessage {
174            realtime_input: RealtimeInputPayload {
175                media_chunks: Vec::new(),
176                audio: Some(Blob {
177                    mime_type: "audio/pcm".to_string(),
178                    data: "AQIDBA==".to_string(),
179                }),
180                video: None,
181                audio_stream_end: None,
182                text: None,
183            },
184        };
185        let json = serde_json::to_string(&msg).unwrap();
186        assert!(json.contains("\"realtimeInput\""));
187        assert!(json.contains("\"audio\""));
188        assert!(json.contains("\"mimeType\""));
189        // Deprecated field should not appear when empty
190        assert!(!json.contains("\"mediaChunks\""));
191    }
192
193    #[test]
194    fn realtime_input_serialization_legacy() {
195        let msg = RealtimeInputMessage {
196            realtime_input: RealtimeInputPayload {
197                media_chunks: vec![MediaChunk {
198                    mime_type: "audio/pcm".to_string(),
199                    data: "AQIDBA==".to_string(),
200                }],
201                audio: None,
202                video: None,
203                audio_stream_end: None,
204                text: None,
205            },
206        };
207        let json = serde_json::to_string(&msg).unwrap();
208        assert!(json.contains("\"mediaChunks\""));
209    }
210
211    #[test]
212    fn parse_session_resumption_update() {
213        let json = r#"{"sessionResumptionUpdate": {"newHandle": "handle-xyz", "resumable": true}}"#;
214        let msg = ServerMessage::parse(json).unwrap();
215        match msg {
216            ServerMessage::SessionResumptionUpdate(sru) => {
217                assert_eq!(
218                    sru.session_resumption_update.new_handle,
219                    Some("handle-xyz".to_string())
220                );
221                assert_eq!(sru.session_resumption_update.resumable, Some(true));
222            }
223            _ => panic!("Expected SessionResumptionUpdate"),
224        }
225    }
226
227    #[test]
228    fn tool_response_serialization() {
229        let msg = ToolResponseMessage {
230            tool_response: ToolResponsePayload {
231                function_responses: vec![FunctionResponse {
232                    name: "get_weather".to_string(),
233                    response: serde_json::json!({"temp": 22}),
234                    id: Some("call-1".to_string()),
235                    scheduling: None,
236                }],
237            },
238        };
239        let json = serde_json::to_string(&msg).unwrap();
240        assert!(json.contains("\"toolResponse\""));
241        assert!(json.contains("\"functionResponses\""));
242    }
243
244    #[test]
245    fn client_content_serialization() {
246        let msg = ClientContentMessage {
247            client_content: ClientContentPayload {
248                turns: vec![Content::user("Hello")],
249                turn_complete: Some(true),
250            },
251        };
252        let json = serde_json::to_string(&msg).unwrap();
253        assert!(json.contains("\"clientContent\""));
254        assert!(json.contains("\"turnComplete\""));
255    }
256
257    #[test]
258    fn activity_signal_serialization() {
259        let msg = ActivitySignalMessage {
260            realtime_input: ActivitySignalPayload {
261                activity_start: Some(ActivityStart {}),
262                activity_end: None,
263            },
264        };
265        let json = serde_json::to_string(&msg).unwrap();
266        assert!(json.contains("\"activityStart\""));
267    }
268
269    #[test]
270    fn voice_activity_type_serialization() {
271        let start = VoiceActivityType::VoiceActivityStart;
272        let json = serde_json::to_string(&start).unwrap();
273        assert_eq!(json, "\"ACTIVITY_START\"");
274        let parsed: VoiceActivityType = serde_json::from_str(&json).unwrap();
275        assert_eq!(parsed, start);
276
277        let end = VoiceActivityType::VoiceActivityEnd;
278        let json = serde_json::to_string(&end).unwrap();
279        assert_eq!(json, "\"ACTIVITY_END\"");
280        let parsed: VoiceActivityType = serde_json::from_str(&json).unwrap();
281        assert_eq!(parsed, end);
282    }
283
284    #[test]
285    fn parse_voice_activity_message() {
286        let json = r#"{"voiceActivity":{"voiceActivityType":"VOICE_ACTIVITY_START"}}"#;
287        let msg = ServerMessage::parse(json).unwrap();
288        match msg {
289            ServerMessage::VoiceActivity(va) => {
290                assert_eq!(
291                    va.voice_activity.voice_activity_type,
292                    Some(VoiceActivityType::VoiceActivityStart)
293                );
294            }
295            _ => panic!("Expected VoiceActivity"),
296        }
297
298        let json = r#"{"voiceActivity":{"voiceActivityType":"VOICE_ACTIVITY_END"}}"#;
299        let msg = ServerMessage::parse(json).unwrap();
300        match msg {
301            ServerMessage::VoiceActivity(va) => {
302                assert_eq!(
303                    va.voice_activity.voice_activity_type,
304                    Some(VoiceActivityType::VoiceActivityEnd)
305                );
306            }
307            _ => panic!("Expected VoiceActivity"),
308        }
309    }
310
311    #[test]
312    fn parse_input_transcription() {
313        let json = r#"{
314            "serverContent": {
315                "inputTranscription": {"text": "Hello world"}
316            }
317        }"#;
318        let msg = ServerMessage::parse(json).unwrap();
319        match msg {
320            ServerMessage::ServerContent(sc) => {
321                let text = sc.server_content.input_transcription.unwrap().text.unwrap();
322                assert_eq!(text, "Hello world");
323            }
324            _ => panic!("Expected ServerContent"),
325        }
326    }
327}