Skip to main content

rustyclaw_core/gateway/protocol/
frames.rs

1//! Frame types and serialization for gateway protocol.
2//!
3//! This module contains the shared types used by both client and server.
4
5use serde::{Deserialize, Serialize};
6
7/// Incoming frame types from client to gateway.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[repr(u8)]
10pub enum ClientFrameType {
11    /// Authentication response with TOTP code.
12    AuthResponse = 0,
13    /// Unlock the vault with password.
14    UnlockVault = 1,
15    /// List all secrets.
16    SecretsList = 2,
17    /// Get a specific secret.
18    SecretsGet = 3,
19    /// Store a secret.
20    SecretsStore = 4,
21    /// Delete a secret.
22    SecretsDelete = 5,
23    /// Peek at a credential (display without exposing value).
24    SecretsPeek = 6,
25    /// Set access policy for a credential.
26    SecretsSetPolicy = 7,
27    /// Enable/disable a credential.
28    SecretsSetDisabled = 8,
29    /// Delete a credential entirely.
30    SecretsDeleteCredential = 9,
31    /// Check if TOTP is configured.
32    SecretsHasTotp = 10,
33    /// Set up TOTP for the vault.
34    SecretsSetupTotp = 11,
35    /// Verify a TOTP code.
36    SecretsVerifyTotp = 12,
37    /// Remove TOTP from the vault.
38    SecretsRemoveTotp = 13,
39    /// Reload configuration.
40    Reload = 14,
41    /// Cancel the current tool loop.
42    Cancel = 15,
43    /// Chat message (default).
44    Chat = 16,
45    /// User response to a tool approval request.
46    ToolApprovalResponse = 17,
47    /// User response to a structured prompt (ask_user tool).
48    UserPromptResponse = 18,
49}
50
51/// Outgoing frame types from gateway to client.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[repr(u8)]
54pub enum ServerFrameType {
55    /// Authentication challenge request.
56    AuthChallenge = 0,
57    /// Authentication result.
58    AuthResult = 1,
59    /// Too many auth attempts, locked out.
60    AuthLocked = 2,
61    /// Hello message on connect.
62    Hello = 3,
63    /// Status update frame.
64    Status = 4,
65    /// Vault unlocked result.
66    VaultUnlocked = 5,
67    /// Secrets list result.
68    SecretsListResult = 6,
69    /// Secrets store result.
70    SecretsStoreResult = 7,
71    /// Secrets get result.
72    SecretsGetResult = 8,
73    /// Secrets delete result.
74    SecretsDeleteResult = 9,
75    /// Secrets peek result.
76    SecretsPeekResult = 10,
77    /// Secrets set policy result.
78    SecretsSetPolicyResult = 11,
79    /// Secrets set disabled result.
80    SecretsSetDisabledResult = 12,
81    /// Secrets delete credential result.
82    SecretsDeleteCredentialResult = 13,
83    /// Secrets has TOTP result.
84    SecretsHasTotpResult = 14,
85    /// Secrets setup TOTP result.
86    SecretsSetupTotpResult = 15,
87    /// Secrets verify TOTP result.
88    SecretsVerifyTotpResult = 16,
89    /// Secrets remove TOTP result.
90    SecretsRemoveTotpResult = 17,
91    /// Reload result.
92    ReloadResult = 18,
93    /// Error frame.
94    Error = 19,
95    /// Info frame.
96    Info = 20,
97    /// Stream start.
98    StreamStart = 21,
99    /// Chunk of response text.
100    Chunk = 22,
101    /// Thinking start (for extended thinking).
102    ThinkingStart = 23,
103    /// Thinking delta (streaming thinking content).
104    ThinkingDelta = 24,
105    /// Thinking end.
106    ThinkingEnd = 25,
107    /// Tool call from model.
108    ToolCall = 26,
109    /// Tool result from execution.
110    ToolResult = 27,
111    /// Response complete.
112    ResponseDone = 28,
113    /// Tool approval request — ask user to approve a tool call.
114    ToolApprovalRequest = 29,
115    /// Structured user prompt request (ask_user tool).
116    UserPromptRequest = 30,
117}
118
119/// Status frame sub-types.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[repr(u8)]
122pub enum StatusType {
123    /// Model is configured.
124    ModelConfigured = 0,
125    /// Credentials loaded.
126    CredentialsLoaded = 1,
127    /// Credentials missing.
128    CredentialsMissing = 2,
129    /// Model connecting.
130    ModelConnecting = 3,
131    /// Model ready.
132    ModelReady = 4,
133    /// Model error.
134    ModelError = 5,
135    /// No model configured.
136    NoModel = 6,
137    /// Vault is locked.
138    VaultLocked = 7,
139}
140
141// ============================================================================
142// Binary Frame Types
143// ============================================================================
144
145/// Generic client frame envelope.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ClientFrame {
148    pub frame_type: ClientFrameType,
149    pub payload: ClientPayload,
150}
151
152/// Payload variants for client frames.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub enum ClientPayload {
155    Empty,
156    AuthChallenge {
157        method: String,
158    },
159    AuthResponse {
160        code: String,
161    },
162    UnlockVault {
163        password: String,
164    },
165    Reload,
166    Chat {
167        messages: Vec<super::types::ChatMessage>,
168    },
169    SecretsList,
170    SecretsGet {
171        key: String,
172    },
173    SecretsStore {
174        key: String,
175        value: String,
176    },
177    SecretsDelete {
178        key: String,
179    },
180    SecretsPeek {
181        name: String,
182    },
183    SecretsSetPolicy {
184        name: String,
185        policy: String,
186        skills: Vec<String>,
187    },
188    SecretsSetDisabled {
189        name: String,
190        disabled: bool,
191    },
192    SecretsDeleteCredential {
193        name: String,
194    },
195    SecretsHasTotp,
196    SecretsSetupTotp,
197    SecretsVerifyTotp {
198        code: String,
199    },
200    SecretsRemoveTotp,
201    ToolApprovalResponse {
202        id: String,
203        approved: bool,
204    },
205    UserPromptResponse {
206        id: String,
207        dismissed: bool,
208        value: crate::user_prompt_types::PromptResponseValue,
209    },
210}
211
212/// Generic server frame envelope.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ServerFrame {
215    pub frame_type: ServerFrameType,
216    pub payload: ServerPayload,
217}
218
219/// Payload variants for server frames.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum ServerPayload {
222    Empty,
223    Hello {
224        agent: String,
225        settings_dir: String,
226        vault_locked: bool,
227        provider: Option<String>,
228        model: Option<String>,
229    },
230    AuthChallenge {
231        method: String,
232    },
233    AuthResult {
234        ok: bool,
235        message: Option<String>,
236        retry: Option<bool>,
237    },
238    AuthLocked {
239        message: String,
240        retry_after: Option<u64>,
241    },
242    Status {
243        status: StatusType,
244        detail: String,
245    },
246    VaultUnlocked {
247        ok: bool,
248        message: Option<String>,
249    },
250    SecretsListResult {
251        ok: bool,
252        entries: Vec<SecretEntryDto>,
253    },
254    SecretsStoreResult {
255        ok: bool,
256        message: String,
257    },
258    SecretsGetResult {
259        ok: bool,
260        key: String,
261        value: Option<String>,
262        message: Option<String>,
263    },
264    SecretsDeleteResult {
265        ok: bool,
266        message: Option<String>,
267    },
268    SecretsPeekResult {
269        ok: bool,
270        fields: Vec<(String, String)>,
271        message: Option<String>,
272    },
273    SecretsSetPolicyResult {
274        ok: bool,
275        message: Option<String>,
276    },
277    SecretsSetDisabledResult {
278        ok: bool,
279        message: Option<String>,
280    },
281    SecretsDeleteCredentialResult {
282        ok: bool,
283        message: Option<String>,
284    },
285    SecretsHasTotpResult {
286        has_totp: bool,
287    },
288    SecretsSetupTotpResult {
289        ok: bool,
290        uri: Option<String>,
291        message: Option<String>,
292    },
293    SecretsVerifyTotpResult {
294        ok: bool,
295        message: Option<String>,
296    },
297    SecretsRemoveTotpResult {
298        ok: bool,
299        message: Option<String>,
300    },
301    ReloadResult {
302        ok: bool,
303        provider: String,
304        model: String,
305        message: Option<String>,
306    },
307    Error {
308        ok: bool,
309        message: String,
310    },
311    Info {
312        message: String,
313    },
314    StreamStart,
315    Chunk {
316        delta: String,
317    },
318    ThinkingStart,
319    ThinkingDelta {
320        delta: String,
321    },
322    ThinkingEnd,
323    ToolCall {
324        id: String,
325        name: String,
326        arguments: String,
327    },
328    ToolResult {
329        id: String,
330        name: String,
331        result: String,
332        is_error: bool,
333    },
334    ResponseDone {
335        ok: bool,
336    },
337    ToolApprovalRequest {
338        id: String,
339        name: String,
340        arguments: String,
341    },
342    UserPromptRequest {
343        id: String,
344        prompt: crate::user_prompt_types::UserPrompt,
345    },
346}
347
348/// DTO for secret entries in list results.
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct SecretEntryDto {
351    pub name: String,
352    pub label: String,
353    pub kind: String,
354    pub policy: String,
355    pub disabled: bool,
356}
357
358// ============================================================================
359// Serialization
360// ============================================================================
361
362/// Serialize a frame to binary using bincode with serde.
363pub fn serialize_frame<T: serde::Serialize>(frame: &T) -> Result<Vec<u8>, String> {
364    bincode::serde::encode_to_vec(frame, bincode::config::standard()).map_err(|e| e.to_string())
365}
366
367/// Deserialize a frame from binary using bincode with serde.
368pub fn deserialize_frame<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
369    let (result, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())
370        .map_err(|e| e.to_string())?;
371    Ok(result)
372}
373
374/// Helper to send a ServerFrame as a binary WebSocket message.
375#[macro_export]
376macro_rules! send_binary_frame {
377    ($writer:expr, $frame:expr) => {{
378        let bytes = $crate::gateway::serialize_frame(&$frame)
379            .map_err(|e| anyhow::anyhow!("Failed to serialize frame: {}", e))?;
380        $writer
381            .send(tokio_tungstenite::tungstenite::Message::Binary(bytes))
382            .await
383            .map_err(|e| anyhow::anyhow!("Failed to send frame: {}", e))
384    }};
385}
386
387/// Helper to parse a client frame from binary WebSocket message bytes.
388#[macro_export]
389macro_rules! parse_binary_client_frame {
390    ($bytes:expr) => {{
391        $crate::gateway::deserialize_frame::<$crate::gateway::ClientFrame>($bytes)
392            .map_err(|e| anyhow::anyhow!("Failed to parse client frame: {}", e))
393    }};
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    mod serialization {
401        use super::*;
402
403        #[test]
404        fn test_server_frame_type_values() {
405            assert_eq!(ServerFrameType::AuthChallenge as u8, 0);
406            assert_eq!(ServerFrameType::AuthResult as u8, 1);
407            assert_eq!(ServerFrameType::AuthLocked as u8, 2);
408            assert_eq!(ServerFrameType::Hello as u8, 3);
409            assert_eq!(ServerFrameType::Status as u8, 4);
410            assert_eq!(ServerFrameType::VaultUnlocked as u8, 5);
411            assert_eq!(ServerFrameType::SecretsListResult as u8, 6);
412            assert_eq!(ServerFrameType::SecretsStoreResult as u8, 7);
413            assert_eq!(ServerFrameType::SecretsGetResult as u8, 8);
414            assert_eq!(ServerFrameType::SecretsDeleteResult as u8, 9);
415            assert_eq!(ServerFrameType::SecretsPeekResult as u8, 10);
416            assert_eq!(ServerFrameType::SecretsSetPolicyResult as u8, 11);
417            assert_eq!(ServerFrameType::SecretsSetDisabledResult as u8, 12);
418            assert_eq!(ServerFrameType::SecretsDeleteCredentialResult as u8, 13);
419            assert_eq!(ServerFrameType::SecretsHasTotpResult as u8, 14);
420            assert_eq!(ServerFrameType::SecretsSetupTotpResult as u8, 15);
421            assert_eq!(ServerFrameType::SecretsVerifyTotpResult as u8, 16);
422            assert_eq!(ServerFrameType::SecretsRemoveTotpResult as u8, 17);
423            assert_eq!(ServerFrameType::ReloadResult as u8, 18);
424            assert_eq!(ServerFrameType::Error as u8, 19);
425            assert_eq!(ServerFrameType::Info as u8, 20);
426            assert_eq!(ServerFrameType::StreamStart as u8, 21);
427            assert_eq!(ServerFrameType::Chunk as u8, 22);
428            assert_eq!(ServerFrameType::ThinkingStart as u8, 23);
429            assert_eq!(ServerFrameType::ThinkingDelta as u8, 24);
430            assert_eq!(ServerFrameType::ThinkingEnd as u8, 25);
431            assert_eq!(ServerFrameType::ToolCall as u8, 26);
432            assert_eq!(ServerFrameType::ToolResult as u8, 27);
433            assert_eq!(ServerFrameType::ResponseDone as u8, 28);
434            assert_eq!(ServerFrameType::ToolApprovalRequest as u8, 29);
435            assert_eq!(ServerFrameType::UserPromptRequest as u8, 30);
436        }
437
438        #[test]
439        fn test_client_frame_type_values() {
440            assert_eq!(ClientFrameType::AuthResponse as u8, 0);
441            assert_eq!(ClientFrameType::UnlockVault as u8, 1);
442            assert_eq!(ClientFrameType::SecretsList as u8, 2);
443            assert_eq!(ClientFrameType::SecretsGet as u8, 3);
444            assert_eq!(ClientFrameType::SecretsStore as u8, 4);
445            assert_eq!(ClientFrameType::SecretsDelete as u8, 5);
446            assert_eq!(ClientFrameType::SecretsPeek as u8, 6);
447            assert_eq!(ClientFrameType::SecretsSetPolicy as u8, 7);
448            assert_eq!(ClientFrameType::SecretsSetDisabled as u8, 8);
449            assert_eq!(ClientFrameType::SecretsDeleteCredential as u8, 9);
450            assert_eq!(ClientFrameType::SecretsHasTotp as u8, 10);
451            assert_eq!(ClientFrameType::SecretsSetupTotp as u8, 11);
452            assert_eq!(ClientFrameType::SecretsVerifyTotp as u8, 12);
453            assert_eq!(ClientFrameType::SecretsRemoveTotp as u8, 13);
454            assert_eq!(ClientFrameType::Reload as u8, 14);
455            assert_eq!(ClientFrameType::Cancel as u8, 15);
456            assert_eq!(ClientFrameType::Chat as u8, 16);
457            assert_eq!(ClientFrameType::ToolApprovalResponse as u8, 17);
458            assert_eq!(ClientFrameType::UserPromptResponse as u8, 18);
459        }
460
461        #[test]
462        fn test_status_type_values() {
463            assert_eq!(StatusType::ModelConfigured as u8, 0);
464            assert_eq!(StatusType::CredentialsLoaded as u8, 1);
465            assert_eq!(StatusType::CredentialsMissing as u8, 2);
466            assert_eq!(StatusType::ModelConnecting as u8, 3);
467            assert_eq!(StatusType::ModelReady as u8, 4);
468            assert_eq!(StatusType::ModelError as u8, 5);
469            assert_eq!(StatusType::NoModel as u8, 6);
470            assert_eq!(StatusType::VaultLocked as u8, 7);
471        }
472
473        #[test]
474        fn test_server_frame_roundtrip_hello() {
475            let frame = ServerFrame {
476                frame_type: ServerFrameType::Hello,
477                payload: ServerPayload::Hello {
478                    agent: "test-agent".into(),
479                    settings_dir: "/tmp/settings".into(),
480                    vault_locked: false,
481                    provider: Some("anthropic".into()),
482                    model: Some("claude-3".into()),
483                },
484            };
485
486            let bytes = serialize_frame(&frame).expect("serialize should succeed");
487            let decoded: ServerFrame =
488                deserialize_frame(&bytes).expect("deserialize should succeed");
489
490            match decoded.payload {
491                ServerPayload::Hello {
492                    agent,
493                    settings_dir,
494                    vault_locked,
495                    provider,
496                    model,
497                } => {
498                    assert_eq!(agent, "test-agent");
499                    assert_eq!(settings_dir, "/tmp/settings");
500                    assert!(!vault_locked);
501                    assert_eq!(provider, Some("anthropic".into()));
502                    assert_eq!(model, Some("claude-3".into()));
503                }
504                _ => panic!("Expected Hello payload"),
505            }
506        }
507
508        #[test]
509        fn test_server_frame_roundtrip_chunk() {
510            let frame = ServerFrame {
511                frame_type: ServerFrameType::Chunk,
512                payload: ServerPayload::Chunk {
513                    delta: "Hello, world!".into(),
514                },
515            };
516
517            let bytes = serialize_frame(&frame).expect("serialize should succeed");
518            let decoded: ServerFrame =
519                deserialize_frame(&bytes).expect("deserialize should succeed");
520
521            match decoded.payload {
522                ServerPayload::Chunk { delta } => {
523                    assert_eq!(delta, "Hello, world!");
524                }
525                _ => panic!("Expected Chunk payload"),
526            }
527        }
528
529        #[test]
530        fn test_server_frame_roundtrip_status() {
531            let frame = ServerFrame {
532                frame_type: ServerFrameType::Status,
533                payload: ServerPayload::Status {
534                    status: StatusType::ModelReady,
535                    detail: "Connected to Claude 3.5 Sonnet".into(),
536                },
537            };
538
539            let bytes = serialize_frame(&frame).expect("serialize should succeed");
540            let decoded: ServerFrame =
541                deserialize_frame(&bytes).expect("deserialize should succeed");
542
543            match decoded.payload {
544                ServerPayload::Status { status, detail } => {
545                    assert_eq!(status, StatusType::ModelReady);
546                    assert_eq!(detail, "Connected to Claude 3.5 Sonnet");
547                }
548                _ => panic!("Expected Status payload"),
549            }
550        }
551
552        #[test]
553        fn test_server_frame_roundtrip_auth_result() {
554            let frame = ServerFrame {
555                frame_type: ServerFrameType::AuthResult,
556                payload: ServerPayload::AuthResult {
557                    ok: true,
558                    message: Some("Authenticated successfully".into()),
559                    retry: None,
560                },
561            };
562
563            let bytes = serialize_frame(&frame).expect("serialize should succeed");
564            let decoded: ServerFrame =
565                deserialize_frame(&bytes).expect("deserialize should succeed");
566
567            match decoded.payload {
568                ServerPayload::AuthResult { ok, message, retry } => {
569                    assert!(ok);
570                    assert_eq!(message, Some("Authenticated successfully".into()));
571                    assert!(retry.is_none());
572                }
573                _ => panic!("Expected AuthResult payload"),
574            }
575        }
576
577        #[test]
578        fn test_client_frame_roundtrip_chat() {
579            let frame = ClientFrame {
580                frame_type: ClientFrameType::Chat,
581                payload: ClientPayload::Empty,
582            };
583
584            let bytes = serialize_frame(&frame).expect("serialize should succeed");
585            let decoded: ClientFrame =
586                deserialize_frame(&bytes).expect("deserialize should succeed");
587
588            assert_eq!(decoded.frame_type, ClientFrameType::Chat);
589            matches!(decoded.payload, ClientPayload::Empty);
590        }
591
592        #[test]
593        fn test_client_frame_roundtrip_secrets_store() {
594            let frame = ClientFrame {
595                frame_type: ClientFrameType::SecretsStore,
596                payload: ClientPayload::SecretsStore {
597                    key: "OPENAI_API_KEY".into(),
598                    value: "sk-test123".into(),
599                },
600            };
601
602            let bytes = serialize_frame(&frame).expect("serialize should succeed");
603            let decoded: ClientFrame =
604                deserialize_frame(&bytes).expect("deserialize should succeed");
605
606            match decoded.payload {
607                ClientPayload::SecretsStore { key, value } => {
608                    assert_eq!(key, "OPENAI_API_KEY");
609                    assert_eq!(value, "sk-test123");
610                }
611                _ => panic!("Expected SecretsStore payload"),
612            }
613        }
614
615        #[test]
616        fn test_secret_entry_dto_roundtrip() {
617            let entry = SecretEntryDto {
618                name: "api_key".into(),
619                label: "OpenAI API Key".into(),
620                kind: "ApiKey".into(),
621                policy: "always".into(),
622                disabled: false,
623            };
624
625            let json = serde_json::to_string(&entry).expect("JSON serialize should succeed");
626            let decoded: SecretEntryDto =
627                serde_json::from_str(&json).expect("JSON deserialize should succeed");
628
629            assert_eq!(decoded.name, "api_key");
630            assert_eq!(decoded.label, "OpenAI API Key");
631            assert_eq!(decoded.kind, "ApiKey");
632            assert_eq!(decoded.policy, "always");
633            assert!(!decoded.disabled);
634        }
635
636        #[test]
637        fn test_user_prompt_response_bincode_roundtrip() {
638            use crate::user_prompt_types::PromptResponseValue;
639
640            let frame = ClientFrame {
641                frame_type: ClientFrameType::UserPromptResponse,
642                payload: ClientPayload::UserPromptResponse {
643                    id: "call_456".into(),
644                    dismissed: false,
645                    value: PromptResponseValue::Text("hello world".into()),
646                },
647            };
648            let bytes = serialize_frame(&frame).expect("serialize should succeed");
649            let decoded: ClientFrame =
650                deserialize_frame(&bytes).expect("deserialize should succeed");
651            match decoded.payload {
652                ClientPayload::UserPromptResponse { id, dismissed, value } => {
653                    assert_eq!(id, "call_456");
654                    assert!(!dismissed);
655                    assert_eq!(value, PromptResponseValue::Text("hello world".into()));
656                }
657                _ => panic!("Expected UserPromptResponse payload"),
658            }
659        }
660
661        #[test]
662        fn test_server_user_prompt_request_bincode_roundtrip() {
663            use crate::user_prompt_types::{UserPrompt, PromptType};
664
665            let prompt = UserPrompt {
666                id: "call_789".into(),
667                title: "What is your name?".into(),
668                description: Some("Please enter your full name".into()),
669                prompt_type: PromptType::TextInput {
670                    placeholder: Some("John Doe".into()),
671                    default: None,
672                },
673            };
674
675            let frame = ServerFrame {
676                frame_type: ServerFrameType::UserPromptRequest,
677                payload: ServerPayload::UserPromptRequest {
678                    id: "call_789".into(),
679                    prompt: prompt.clone(),
680                },
681            };
682
683            let bytes = serialize_frame(&frame).expect("serialize should succeed");
684            let decoded: ServerFrame =
685                deserialize_frame(&bytes).expect("deserialize should succeed");
686
687            assert_eq!(decoded.frame_type, ServerFrameType::UserPromptRequest);
688            match decoded.payload {
689                ServerPayload::UserPromptRequest { id, prompt: p } => {
690                    assert_eq!(id, "call_789");
691                    assert_eq!(p.title, "What is your name?");
692                    assert_eq!(p.description, Some("Please enter your full name".into()));
693                    assert!(matches!(p.prompt_type, PromptType::TextInput { .. }));
694                }
695                _ => panic!("Expected UserPromptRequest payload"),
696            }
697        }
698
699        #[test]
700        fn test_client_frame_roundtrip_auth_response() {
701            let frame = ClientFrame {
702                frame_type: ClientFrameType::AuthResponse,
703                payload: ClientPayload::AuthResponse {
704                    code: "123456".into(),
705                },
706            };
707
708            let bytes = serialize_frame(&frame).expect("serialize should succeed");
709            let decoded: ClientFrame =
710                deserialize_frame(&bytes).expect("deserialize should succeed");
711
712            assert_eq!(decoded.frame_type, ClientFrameType::AuthResponse);
713            match decoded.payload {
714                ClientPayload::AuthResponse { code } => {
715                    assert_eq!(code, "123456");
716                }
717                _ => panic!("Expected AuthResponse payload"),
718            }
719        }
720
721        #[test]
722        fn test_server_frame_roundtrip_auth_challenge() {
723            let frame = ServerFrame {
724                frame_type: ServerFrameType::AuthChallenge,
725                payload: ServerPayload::AuthChallenge {
726                    method: "totp".into(),
727                },
728            };
729
730            let bytes = serialize_frame(&frame).expect("serialize should succeed");
731            let decoded: ServerFrame =
732                deserialize_frame(&bytes).expect("deserialize should succeed");
733
734            assert_eq!(decoded.frame_type, ServerFrameType::AuthChallenge);
735            match decoded.payload {
736                ServerPayload::AuthChallenge { method } => {
737                    assert_eq!(method, "totp");
738                }
739                _ => panic!("Expected AuthChallenge payload"),
740            }
741        }
742
743        #[test]
744        fn test_server_tool_call_bincode_roundtrip() {
745            let frame = ServerFrame {
746                frame_type: ServerFrameType::ToolCall,
747                payload: ServerPayload::ToolCall {
748                    id: "call_001".into(),
749                    name: "read_file".into(),
750                    arguments: r#"{"path":"/tmp/test"}"#.into(),
751                },
752            };
753
754            let bytes = serialize_frame(&frame).expect("serialize should succeed");
755            let decoded: ServerFrame =
756                deserialize_frame(&bytes).expect("deserialize should succeed");
757
758            match decoded.payload {
759                ServerPayload::ToolCall { id, name, arguments } => {
760                    assert_eq!(id, "call_001");
761                    assert_eq!(name, "read_file");
762                    assert_eq!(arguments, r#"{"path":"/tmp/test"}"#);
763                }
764                _ => panic!("Expected ToolCall payload"),
765            }
766        }
767    }
768}