claude_codes/
io.rs

1//! Core message types for Claude communication.
2//!
3//! This module defines the primary message structures used in the Claude protocol:
4//!
5//! - [`ClaudeInput`] - Messages sent to Claude
6//! - [`ClaudeOutput`] - Messages received from Claude
7//! - [`ContentBlock`] - Different types of content within messages
8//!
9//! # Message Flow
10//!
11//! 1. Create a [`ClaudeInput`] with your query
12//! 2. Send it to Claude via a client
13//! 3. Receive [`ClaudeOutput`] messages in response
14//! 4. Handle different output types (System, Assistant, Result)
15//!
16//! # Example
17//!
18//! ```
19//! use claude_codes::{ClaudeInput, ClaudeOutput};
20//!
21//! // Create an input message
22//! let input = ClaudeInput::user_message("Hello, Claude!", uuid::Uuid::new_v4());
23//!
24//! // Parse an output message
25//! let json = r#"{"type":"assistant","message":{"role":"assistant","content":[]}}"#;
26//! match ClaudeOutput::parse_json(json) {
27//!     Ok(output) => println!("Got: {}", output.message_type()),
28//!     Err(e) => eprintln!("Parse error: {}", e),
29//! }
30//! ```
31
32use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use uuid::Uuid;
36
37/// Serialize an optional UUID as a string
38fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
39where
40    S: Serializer,
41{
42    match uuid {
43        Some(id) => serializer.serialize_str(&id.to_string()),
44        None => serializer.serialize_none(),
45    }
46}
47
48/// Deserialize an optional UUID from a string
49fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
50where
51    D: Deserializer<'de>,
52{
53    let opt_str: Option<String> = Option::deserialize(deserializer)?;
54    match opt_str {
55        Some(s) => Uuid::parse_str(&s)
56            .map(Some)
57            .map_err(serde::de::Error::custom),
58        None => Ok(None),
59    }
60}
61
62/// Top-level enum for all possible Claude input messages
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ClaudeInput {
66    /// User message input
67    User(UserMessage),
68
69    /// Control request (for initialization handshake)
70    ControlRequest(ControlRequest),
71
72    /// Control response (for tool permission responses)
73    ControlResponse(ControlResponse),
74
75    /// Raw JSON for untyped messages
76    #[serde(untagged)]
77    Raw(Value),
78}
79
80/// Error type for parsing failures that preserves the raw JSON
81#[derive(Debug, Clone)]
82pub struct ParseError {
83    /// The raw JSON value that failed to parse
84    pub raw_json: Value,
85    /// The underlying serde error message
86    pub error_message: String,
87}
88
89impl fmt::Display for ParseError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
92    }
93}
94
95impl std::error::Error for ParseError {}
96
97/// Top-level enum for all possible Claude output messages
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(tag = "type", rename_all = "snake_case")]
100pub enum ClaudeOutput {
101    /// System initialization message
102    System(SystemMessage),
103
104    /// User message echoed back
105    User(UserMessage),
106
107    /// Assistant response
108    Assistant(AssistantMessage),
109
110    /// Result message (completion of a query)
111    Result(ResultMessage),
112
113    /// Control request from CLI (tool permissions, hooks, etc.)
114    ControlRequest(ControlRequest),
115
116    /// Control response from CLI (ack for initialization, etc.)
117    ControlResponse(ControlResponse),
118}
119
120/// User message
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UserMessage {
123    pub message: MessageContent,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    #[serde(
126        serialize_with = "serialize_optional_uuid",
127        deserialize_with = "deserialize_optional_uuid"
128    )]
129    pub session_id: Option<Uuid>,
130}
131
132/// Message content with role
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MessageContent {
135    pub role: String,
136    #[serde(deserialize_with = "deserialize_content_blocks")]
137    pub content: Vec<ContentBlock>,
138}
139
140/// Deserialize content blocks that can be either a string or array
141fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
142where
143    D: Deserializer<'de>,
144{
145    let value: Value = Value::deserialize(deserializer)?;
146    match value {
147        Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
148        Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
149        _ => Err(serde::de::Error::custom(
150            "content must be a string or array",
151        )),
152    }
153}
154
155/// System message with metadata
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SystemMessage {
158    pub subtype: String,
159    #[serde(flatten)]
160    pub data: Value, // Captures all other fields
161}
162
163impl SystemMessage {
164    /// Check if this is an init message
165    pub fn is_init(&self) -> bool {
166        self.subtype == "init"
167    }
168
169    /// Check if this is a status message
170    pub fn is_status(&self) -> bool {
171        self.subtype == "status"
172    }
173
174    /// Check if this is a compact_boundary message
175    pub fn is_compact_boundary(&self) -> bool {
176        self.subtype == "compact_boundary"
177    }
178
179    /// Try to parse as an init message
180    pub fn as_init(&self) -> Option<InitMessage> {
181        if self.subtype != "init" {
182            return None;
183        }
184        serde_json::from_value(self.data.clone()).ok()
185    }
186
187    /// Try to parse as a status message
188    pub fn as_status(&self) -> Option<StatusMessage> {
189        if self.subtype != "status" {
190            return None;
191        }
192        serde_json::from_value(self.data.clone()).ok()
193    }
194
195    /// Try to parse as a compact_boundary message
196    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
197        if self.subtype != "compact_boundary" {
198            return None;
199        }
200        serde_json::from_value(self.data.clone()).ok()
201    }
202}
203
204/// Init system message data - sent at session start
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct InitMessage {
207    /// Session identifier
208    pub session_id: String,
209    /// Current working directory
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub cwd: Option<String>,
212    /// Model being used
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub model: Option<String>,
215    /// List of available tools
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    pub tools: Vec<String>,
218    /// MCP servers configured
219    #[serde(default, skip_serializing_if = "Vec::is_empty")]
220    pub mcp_servers: Vec<Value>,
221}
222
223/// Status system message - sent during operations like context compaction
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct StatusMessage {
226    /// Session identifier
227    pub session_id: String,
228    /// Current status (e.g., "compacting") or null when complete
229    pub status: Option<String>,
230    /// Unique identifier for this message
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub uuid: Option<String>,
233}
234
235/// Compact boundary message - marks where context compaction occurred
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct CompactBoundaryMessage {
238    /// Session identifier
239    pub session_id: String,
240    /// Metadata about the compaction
241    pub compact_metadata: CompactMetadata,
242    /// Unique identifier for this message
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub uuid: Option<String>,
245}
246
247/// Metadata about context compaction
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CompactMetadata {
250    /// Number of tokens before compaction
251    pub pre_tokens: u64,
252    /// What triggered the compaction ("auto" or "manual")
253    pub trigger: String,
254}
255
256/// Assistant message
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct AssistantMessage {
259    pub message: AssistantMessageContent,
260    pub session_id: String,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub uuid: Option<String>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub parent_tool_use_id: Option<String>,
265}
266
267/// Nested message content for assistant messages
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct AssistantMessageContent {
270    pub id: String,
271    pub role: String,
272    pub model: String,
273    pub content: Vec<ContentBlock>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub stop_reason: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub stop_sequence: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub usage: Option<AssistantUsage>,
280}
281
282/// Usage information for assistant messages
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct AssistantUsage {
285    /// Number of input tokens
286    #[serde(default)]
287    pub input_tokens: u32,
288
289    /// Number of output tokens
290    #[serde(default)]
291    pub output_tokens: u32,
292
293    /// Tokens used to create cache
294    #[serde(default)]
295    pub cache_creation_input_tokens: u32,
296
297    /// Tokens read from cache
298    #[serde(default)]
299    pub cache_read_input_tokens: u32,
300
301    /// Service tier used (e.g., "standard")
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub service_tier: Option<String>,
304
305    /// Detailed cache creation breakdown
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub cache_creation: Option<CacheCreationDetails>,
308}
309
310/// Detailed cache creation information
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct CacheCreationDetails {
313    /// Ephemeral 1-hour input tokens
314    #[serde(default)]
315    pub ephemeral_1h_input_tokens: u32,
316
317    /// Ephemeral 5-minute input tokens
318    #[serde(default)]
319    pub ephemeral_5m_input_tokens: u32,
320}
321
322/// Content blocks for messages
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(tag = "type", rename_all = "snake_case")]
325pub enum ContentBlock {
326    Text(TextBlock),
327    Image(ImageBlock),
328    Thinking(ThinkingBlock),
329    ToolUse(ToolUseBlock),
330    ToolResult(ToolResultBlock),
331}
332
333/// Text content block
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct TextBlock {
336    pub text: String,
337}
338
339/// Image content block (follows Anthropic API structure)
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ImageBlock {
342    pub source: ImageSource,
343}
344
345/// Image source information
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ImageSource {
348    #[serde(rename = "type")]
349    pub source_type: String, // "base64"
350    pub media_type: String, // e.g., "image/jpeg", "image/png"
351    pub data: String,       // Base64-encoded image data
352}
353
354/// Thinking content block
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ThinkingBlock {
357    pub thinking: String,
358    pub signature: String,
359}
360
361/// Tool use content block
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ToolUseBlock {
364    pub id: String,
365    pub name: String,
366    pub input: Value,
367}
368
369impl ToolUseBlock {
370    /// Try to parse the input as a typed ToolInput.
371    ///
372    /// This attempts to deserialize the raw JSON input into a strongly-typed
373    /// `ToolInput` enum variant. Returns `None` if parsing fails.
374    ///
375    /// # Example
376    ///
377    /// ```
378    /// use claude_codes::{ToolUseBlock, ToolInput};
379    /// use serde_json::json;
380    ///
381    /// let block = ToolUseBlock {
382    ///     id: "toolu_123".to_string(),
383    ///     name: "Bash".to_string(),
384    ///     input: json!({"command": "ls -la"}),
385    /// };
386    ///
387    /// if let Some(ToolInput::Bash(bash)) = block.typed_input() {
388    ///     assert_eq!(bash.command, "ls -la");
389    /// }
390    /// ```
391    pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
392        serde_json::from_value(self.input.clone()).ok()
393    }
394
395    /// Parse the input as a typed ToolInput, returning an error on failure.
396    ///
397    /// Unlike `typed_input()`, this method returns the parsing error for debugging.
398    pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
399        serde_json::from_value(self.input.clone())
400    }
401}
402
403/// Tool result content block
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ToolResultBlock {
406    pub tool_use_id: String,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub content: Option<ToolResultContent>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub is_error: Option<bool>,
411}
412
413/// Tool result content type
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(untagged)]
416pub enum ToolResultContent {
417    Text(String),
418    Structured(Vec<Value>),
419}
420
421/// Result message for completed queries
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ResultMessage {
424    pub subtype: ResultSubtype,
425    pub is_error: bool,
426    pub duration_ms: u64,
427    pub duration_api_ms: u64,
428    pub num_turns: i32,
429
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub result: Option<String>,
432
433    pub session_id: String,
434    pub total_cost_usd: f64,
435
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub usage: Option<UsageInfo>,
438
439    /// Tools that were blocked due to permission denials during the session
440    #[serde(default)]
441    pub permission_denials: Vec<PermissionDenial>,
442
443    /// Error messages when `is_error` is true.
444    ///
445    /// Contains human-readable error strings (e.g., "No conversation found with session ID: ...").
446    /// This allows typed access to error conditions without needing to serialize to JSON and search.
447    #[serde(default)]
448    pub errors: Vec<String>,
449
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub uuid: Option<String>,
452}
453
454/// A record of a tool permission that was denied during the session.
455///
456/// This is included in `ResultMessage.permission_denials` to provide a summary
457/// of all permission denials that occurred.
458#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
459pub struct PermissionDenial {
460    /// The name of the tool that was blocked (e.g., "Bash", "Write")
461    pub tool_name: String,
462
463    /// The input that was passed to the tool
464    pub tool_input: Value,
465
466    /// The unique identifier for this tool use request
467    pub tool_use_id: String,
468}
469
470/// Result subtypes
471#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case")]
473pub enum ResultSubtype {
474    Success,
475    ErrorMaxTurns,
476    ErrorDuringExecution,
477}
478
479/// MCP Server configuration types
480#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(tag = "type", rename_all = "snake_case")]
482pub enum McpServerConfig {
483    Stdio(McpStdioServerConfig),
484    Sse(McpSseServerConfig),
485    Http(McpHttpServerConfig),
486}
487
488/// MCP stdio server configuration
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct McpStdioServerConfig {
491    pub command: String,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub args: Option<Vec<String>>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub env: Option<std::collections::HashMap<String, String>>,
496}
497
498/// MCP SSE server configuration
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct McpSseServerConfig {
501    pub url: String,
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub headers: Option<std::collections::HashMap<String, String>>,
504}
505
506/// MCP HTTP server configuration
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct McpHttpServerConfig {
509    pub url: String,
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub headers: Option<std::collections::HashMap<String, String>>,
512}
513
514/// Permission mode for Claude operations
515#[derive(Debug, Clone, Serialize, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub enum PermissionMode {
518    Default,
519    AcceptEdits,
520    BypassPermissions,
521    Plan,
522}
523
524// ============================================================================
525// Control Protocol Types (for bidirectional tool approval)
526// ============================================================================
527
528/// Control request from CLI (tool permission requests, hooks, etc.)
529///
530/// When using `--permission-prompt-tool stdio`, the CLI sends these requests
531/// asking for approval before executing tools. The SDK must respond with a
532/// [`ControlResponse`].
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct ControlRequest {
535    /// Unique identifier for this request (used to correlate responses)
536    pub request_id: String,
537    /// The request payload
538    pub request: ControlRequestPayload,
539}
540
541/// Control request payload variants
542#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(tag = "subtype", rename_all = "snake_case")]
544pub enum ControlRequestPayload {
545    /// Tool permission request - Claude wants to use a tool
546    CanUseTool(ToolPermissionRequest),
547    /// Hook callback request
548    HookCallback(HookCallbackRequest),
549    /// MCP message request
550    McpMessage(McpMessageRequest),
551    /// Initialize request (sent by SDK to CLI)
552    Initialize(InitializeRequest),
553}
554
555/// A suggested permission for tool approval.
556///
557/// When Claude requests tool permission, it may include suggestions for
558/// permissions that could be granted to avoid repeated prompts for similar
559/// actions. The format varies based on the suggestion type:
560///
561/// - `setMode`: `{"type": "setMode", "mode": "acceptEdits", "destination": "session"}`
562/// - `addRules`: `{"type": "addRules", "rules": [...], "behavior": "allow", "destination": "session"}`
563///
564/// Use the helper methods to access common fields.
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
566pub struct PermissionSuggestion {
567    /// The type of suggestion (e.g., "setMode", "addRules")
568    #[serde(rename = "type")]
569    pub suggestion_type: String,
570    /// Where to apply this permission (e.g., "session", "project")
571    pub destination: String,
572    /// The permission mode (for setMode type)
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub mode: Option<String>,
575    /// The behavior (for addRules type, e.g., "allow")
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub behavior: Option<String>,
578    /// The rules to add (for addRules type)
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub rules: Option<Vec<Value>>,
581}
582
583/// Tool permission request details
584///
585/// This is sent when Claude wants to use a tool. The SDK should evaluate
586/// the request and respond with allow/deny using the ergonomic builder methods.
587///
588/// # Example
589///
590/// ```
591/// use claude_codes::{ToolPermissionRequest, ControlResponse};
592/// use serde_json::json;
593///
594/// fn handle_permission(req: &ToolPermissionRequest, request_id: &str) -> ControlResponse {
595///     // Block dangerous bash commands
596///     if req.tool_name == "Bash" {
597///         if let Some(cmd) = req.input.get("command").and_then(|v| v.as_str()) {
598///             if cmd.contains("rm -rf") {
599///                 return req.deny("Dangerous command blocked", request_id);
600///             }
601///         }
602///     }
603///
604///     // Allow everything else
605///     req.allow(request_id)
606/// }
607/// ```
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct ToolPermissionRequest {
610    /// Name of the tool Claude wants to use (e.g., "Bash", "Write", "Read")
611    pub tool_name: String,
612    /// Input parameters for the tool
613    pub input: Value,
614    /// Suggested permissions that could be granted to avoid repeated prompts
615    #[serde(default)]
616    pub permission_suggestions: Vec<PermissionSuggestion>,
617    /// Path that was blocked (if this is a retry after path-based denial)
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub blocked_path: Option<String>,
620}
621
622impl ToolPermissionRequest {
623    /// Allow the tool to execute with its original input.
624    ///
625    /// # Example
626    /// ```
627    /// # use claude_codes::ToolPermissionRequest;
628    /// # use serde_json::json;
629    /// let req = ToolPermissionRequest {
630    ///     tool_name: "Read".to_string(),
631    ///     input: json!({"file_path": "/tmp/test.txt"}),
632    ///     permission_suggestions: vec![],
633    ///     blocked_path: None,
634    /// };
635    /// let response = req.allow("req-123");
636    /// ```
637    pub fn allow(&self, request_id: &str) -> ControlResponse {
638        ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
639    }
640
641    /// Allow the tool to execute with modified input.
642    ///
643    /// Use this to sanitize or redirect tool inputs. For example, redirecting
644    /// file writes to a safe directory.
645    ///
646    /// # Example
647    /// ```
648    /// # use claude_codes::ToolPermissionRequest;
649    /// # use serde_json::json;
650    /// let req = ToolPermissionRequest {
651    ///     tool_name: "Write".to_string(),
652    ///     input: json!({"file_path": "/etc/passwd", "content": "test"}),
653    ///     permission_suggestions: vec![],
654    ///     blocked_path: None,
655    /// };
656    /// // Redirect to safe location
657    /// let safe_input = json!({"file_path": "/tmp/safe/passwd", "content": "test"});
658    /// let response = req.allow_with(safe_input, "req-123");
659    /// ```
660    pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
661        ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
662    }
663
664    /// Allow with updated permissions list.
665    pub fn allow_with_permissions(
666        &self,
667        modified_input: Value,
668        permissions: Vec<Value>,
669        request_id: &str,
670    ) -> ControlResponse {
671        ControlResponse::from_result(
672            request_id,
673            PermissionResult::allow_with_permissions(modified_input, permissions),
674        )
675    }
676
677    /// Deny the tool execution.
678    ///
679    /// The message will be shown to Claude, who may try a different approach.
680    ///
681    /// # Example
682    /// ```
683    /// # use claude_codes::ToolPermissionRequest;
684    /// # use serde_json::json;
685    /// let req = ToolPermissionRequest {
686    ///     tool_name: "Bash".to_string(),
687    ///     input: json!({"command": "sudo rm -rf /"}),
688    ///     permission_suggestions: vec![],
689    ///     blocked_path: None,
690    /// };
691    /// let response = req.deny("Dangerous command blocked by policy", "req-123");
692    /// ```
693    pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
694        ControlResponse::from_result(request_id, PermissionResult::deny(message))
695    }
696
697    /// Deny the tool execution and stop the entire session.
698    ///
699    /// Use this for severe policy violations that should halt all processing.
700    pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
701        ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
702    }
703}
704
705/// Result of a permission decision
706///
707/// This type represents the decision made by the permission callback.
708/// It can be serialized directly into the control response format.
709#[derive(Debug, Clone, Serialize, Deserialize)]
710#[serde(tag = "behavior", rename_all = "snake_case")]
711pub enum PermissionResult {
712    /// Allow the tool to execute
713    Allow {
714        /// The (possibly modified) input to pass to the tool
715        #[serde(rename = "updatedInput")]
716        updated_input: Value,
717        /// Optional updated permissions list
718        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
719        updated_permissions: Option<Vec<Value>>,
720    },
721    /// Deny the tool execution
722    Deny {
723        /// Message explaining why the tool was denied
724        message: String,
725        /// If true, stop the entire session
726        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
727        interrupt: bool,
728    },
729}
730
731impl PermissionResult {
732    /// Create an allow result with the given input
733    pub fn allow(input: Value) -> Self {
734        PermissionResult::Allow {
735            updated_input: input,
736            updated_permissions: None,
737        }
738    }
739
740    /// Create an allow result with permissions
741    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
742        PermissionResult::Allow {
743            updated_input: input,
744            updated_permissions: Some(permissions),
745        }
746    }
747
748    /// Create a deny result
749    pub fn deny(message: impl Into<String>) -> Self {
750        PermissionResult::Deny {
751            message: message.into(),
752            interrupt: false,
753        }
754    }
755
756    /// Create a deny result that also interrupts the session
757    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
758        PermissionResult::Deny {
759            message: message.into(),
760            interrupt: true,
761        }
762    }
763}
764
765/// Hook callback request
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct HookCallbackRequest {
768    pub callback_id: String,
769    pub input: Value,
770    #[serde(skip_serializing_if = "Option::is_none")]
771    pub tool_use_id: Option<String>,
772}
773
774/// MCP message request
775#[derive(Debug, Clone, Serialize, Deserialize)]
776pub struct McpMessageRequest {
777    pub server_name: String,
778    pub message: Value,
779}
780
781/// Initialize request (SDK -> CLI)
782#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct InitializeRequest {
784    #[serde(skip_serializing_if = "Option::is_none")]
785    pub hooks: Option<Value>,
786}
787
788/// Control response to CLI
789///
790/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
791/// constructed directly for other control request types.
792#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct ControlResponse {
794    /// The request ID this response corresponds to
795    pub response: ControlResponsePayload,
796}
797
798impl ControlResponse {
799    /// Create a success response from a PermissionResult
800    ///
801    /// This is the preferred way to construct permission responses.
802    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
803        // Serialize the PermissionResult to Value for the response
804        let response_value = serde_json::to_value(&result)
805            .expect("PermissionResult serialization should never fail");
806        ControlResponse {
807            response: ControlResponsePayload::Success {
808                request_id: request_id.to_string(),
809                response: Some(response_value),
810            },
811        }
812    }
813
814    /// Create a success response with the given payload (raw Value)
815    pub fn success(request_id: &str, response_data: Value) -> Self {
816        ControlResponse {
817            response: ControlResponsePayload::Success {
818                request_id: request_id.to_string(),
819                response: Some(response_data),
820            },
821        }
822    }
823
824    /// Create an empty success response (for acks)
825    pub fn success_empty(request_id: &str) -> Self {
826        ControlResponse {
827            response: ControlResponsePayload::Success {
828                request_id: request_id.to_string(),
829                response: None,
830            },
831        }
832    }
833
834    /// Create an error response
835    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
836        ControlResponse {
837            response: ControlResponsePayload::Error {
838                request_id: request_id.to_string(),
839                error: error_message.into(),
840            },
841        }
842    }
843}
844
845/// Control response payload
846#[derive(Debug, Clone, Serialize, Deserialize)]
847#[serde(tag = "subtype", rename_all = "snake_case")]
848pub enum ControlResponsePayload {
849    Success {
850        request_id: String,
851        #[serde(skip_serializing_if = "Option::is_none")]
852        response: Option<Value>,
853    },
854    Error {
855        request_id: String,
856        error: String,
857    },
858}
859
860/// Wrapper for outgoing control responses (includes type tag)
861#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct ControlResponseMessage {
863    #[serde(rename = "type")]
864    pub message_type: String,
865    pub response: ControlResponsePayload,
866}
867
868impl From<ControlResponse> for ControlResponseMessage {
869    fn from(resp: ControlResponse) -> Self {
870        ControlResponseMessage {
871            message_type: "control_response".to_string(),
872            response: resp.response,
873        }
874    }
875}
876
877/// Wrapper for outgoing control requests (includes type tag)
878#[derive(Debug, Clone, Serialize, Deserialize)]
879pub struct ControlRequestMessage {
880    #[serde(rename = "type")]
881    pub message_type: String,
882    pub request_id: String,
883    pub request: ControlRequestPayload,
884}
885
886impl ControlRequestMessage {
887    /// Create an initialization request to send to CLI
888    pub fn initialize(request_id: impl Into<String>) -> Self {
889        ControlRequestMessage {
890            message_type: "control_request".to_string(),
891            request_id: request_id.into(),
892            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
893        }
894    }
895
896    /// Create an initialization request with hooks configuration
897    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
898        ControlRequestMessage {
899            message_type: "control_request".to_string(),
900            request_id: request_id.into(),
901            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
902        }
903    }
904}
905
906/// Usage information for the request
907#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct UsageInfo {
909    pub input_tokens: u32,
910    pub cache_creation_input_tokens: u32,
911    pub cache_read_input_tokens: u32,
912    pub output_tokens: u32,
913    pub server_tool_use: ServerToolUse,
914    pub service_tier: String,
915}
916
917/// Server tool usage information
918#[derive(Debug, Clone, Serialize, Deserialize)]
919pub struct ServerToolUse {
920    pub web_search_requests: u32,
921}
922
923impl ClaudeInput {
924    /// Create a simple text user message
925    pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
926        ClaudeInput::User(UserMessage {
927            message: MessageContent {
928                role: "user".to_string(),
929                content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
930            },
931            session_id: Some(session_id),
932        })
933    }
934
935    /// Create a user message with content blocks
936    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
937        ClaudeInput::User(UserMessage {
938            message: MessageContent {
939                role: "user".to_string(),
940                content: blocks,
941            },
942            session_id: Some(session_id),
943        })
944    }
945
946    /// Create a user message with an image and optional text
947    /// Only supports JPEG, PNG, GIF, and WebP media types
948    pub fn user_message_with_image(
949        image_data: String,
950        media_type: String,
951        text: Option<String>,
952        session_id: Uuid,
953    ) -> Result<Self, String> {
954        // Validate media type
955        let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
956
957        if !valid_types.contains(&media_type.as_str()) {
958            return Err(format!(
959                "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
960                media_type
961            ));
962        }
963
964        let mut blocks = vec![ContentBlock::Image(ImageBlock {
965            source: ImageSource {
966                source_type: "base64".to_string(),
967                media_type,
968                data: image_data,
969            },
970        })];
971
972        if let Some(text_content) = text {
973            blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
974        }
975
976        Ok(Self::user_message_blocks(blocks, session_id))
977    }
978}
979
980impl ClaudeOutput {
981    /// Get the message type as a string
982    pub fn message_type(&self) -> String {
983        match self {
984            ClaudeOutput::System(_) => "system".to_string(),
985            ClaudeOutput::User(_) => "user".to_string(),
986            ClaudeOutput::Assistant(_) => "assistant".to_string(),
987            ClaudeOutput::Result(_) => "result".to_string(),
988            ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
989            ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
990        }
991    }
992
993    /// Check if this is a control request (tool permission request)
994    pub fn is_control_request(&self) -> bool {
995        matches!(self, ClaudeOutput::ControlRequest(_))
996    }
997
998    /// Check if this is a control response
999    pub fn is_control_response(&self) -> bool {
1000        matches!(self, ClaudeOutput::ControlResponse(_))
1001    }
1002
1003    /// Get the control request if this is one
1004    pub fn as_control_request(&self) -> Option<&ControlRequest> {
1005        match self {
1006            ClaudeOutput::ControlRequest(req) => Some(req),
1007            _ => None,
1008        }
1009    }
1010
1011    /// Check if this is a result with error
1012    pub fn is_error(&self) -> bool {
1013        matches!(self, ClaudeOutput::Result(r) if r.is_error)
1014    }
1015
1016    /// Check if this is an assistant message
1017    pub fn is_assistant_message(&self) -> bool {
1018        matches!(self, ClaudeOutput::Assistant(_))
1019    }
1020
1021    /// Check if this is a system message
1022    pub fn is_system_message(&self) -> bool {
1023        matches!(self, ClaudeOutput::System(_))
1024    }
1025
1026    /// Check if this is a system init message
1027    ///
1028    /// # Example
1029    /// ```
1030    /// use claude_codes::ClaudeOutput;
1031    ///
1032    /// let json = r#"{"type":"system","subtype":"init","session_id":"abc"}"#;
1033    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1034    /// assert!(output.is_system_init());
1035    /// ```
1036    pub fn is_system_init(&self) -> bool {
1037        matches!(self, ClaudeOutput::System(sys) if sys.is_init())
1038    }
1039
1040    /// Get the session ID from any message type that has one.
1041    ///
1042    /// Returns the session ID from System, Assistant, or Result messages.
1043    /// Returns `None` for User, ControlRequest, and ControlResponse messages.
1044    ///
1045    /// # Example
1046    /// ```
1047    /// use claude_codes::ClaudeOutput;
1048    ///
1049    /// let json = r#"{"type":"result","subtype":"success","is_error":false,
1050    ///     "duration_ms":100,"duration_api_ms":200,"num_turns":1,
1051    ///     "session_id":"my-session","total_cost_usd":0.01}"#;
1052    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1053    /// assert_eq!(output.session_id(), Some("my-session"));
1054    /// ```
1055    pub fn session_id(&self) -> Option<&str> {
1056        match self {
1057            ClaudeOutput::System(sys) => sys.data.get("session_id").and_then(|v| v.as_str()),
1058            ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
1059            ClaudeOutput::Result(res) => Some(&res.session_id),
1060            ClaudeOutput::User(_) => None,
1061            ClaudeOutput::ControlRequest(_) => None,
1062            ClaudeOutput::ControlResponse(_) => None,
1063        }
1064    }
1065
1066    /// Get a specific tool use by name from an assistant message.
1067    ///
1068    /// Returns the first `ToolUseBlock` with the given name, or `None` if this
1069    /// is not an assistant message or doesn't contain the specified tool.
1070    ///
1071    /// # Example
1072    /// ```
1073    /// use claude_codes::ClaudeOutput;
1074    ///
1075    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1076    ///     "model":"claude-3","content":[{"type":"tool_use","id":"tu_1",
1077    ///     "name":"Bash","input":{"command":"ls"}}]},"session_id":"abc"}"#;
1078    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1079    ///
1080    /// if let Some(bash) = output.as_tool_use("Bash") {
1081    ///     assert_eq!(bash.name, "Bash");
1082    /// }
1083    /// ```
1084    pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
1085        match self {
1086            ClaudeOutput::Assistant(ass) => {
1087                ass.message.content.iter().find_map(|block| match block {
1088                    ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
1089                    _ => None,
1090                })
1091            }
1092            _ => None,
1093        }
1094    }
1095
1096    /// Get all tool uses from an assistant message.
1097    ///
1098    /// Returns an iterator over all `ToolUseBlock`s in the message, or an empty
1099    /// iterator if this is not an assistant message.
1100    ///
1101    /// # Example
1102    /// ```
1103    /// use claude_codes::ClaudeOutput;
1104    ///
1105    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1106    ///     "model":"claude-3","content":[
1107    ///         {"type":"tool_use","id":"tu_1","name":"Read","input":{"file_path":"/tmp/a"}},
1108    ///         {"type":"tool_use","id":"tu_2","name":"Write","input":{"file_path":"/tmp/b","content":"x"}}
1109    ///     ]},"session_id":"abc"}"#;
1110    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1111    ///
1112    /// let tools: Vec<_> = output.tool_uses().collect();
1113    /// assert_eq!(tools.len(), 2);
1114    /// ```
1115    pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
1116        let content = match self {
1117            ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
1118            _ => None,
1119        };
1120
1121        content
1122            .into_iter()
1123            .flat_map(|c| c.iter())
1124            .filter_map(|block| match block {
1125                ContentBlock::ToolUse(tu) => Some(tu),
1126                _ => None,
1127            })
1128    }
1129
1130    /// Get text content from an assistant message.
1131    ///
1132    /// Returns the concatenated text from all text blocks in the message,
1133    /// or `None` if this is not an assistant message or has no text content.
1134    ///
1135    /// # Example
1136    /// ```
1137    /// use claude_codes::ClaudeOutput;
1138    ///
1139    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1140    ///     "model":"claude-3","content":[{"type":"text","text":"Hello, world!"}]},
1141    ///     "session_id":"abc"}"#;
1142    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1143    /// assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1144    /// ```
1145    pub fn text_content(&self) -> Option<String> {
1146        match self {
1147            ClaudeOutput::Assistant(ass) => {
1148                let texts: Vec<&str> = ass
1149                    .message
1150                    .content
1151                    .iter()
1152                    .filter_map(|block| match block {
1153                        ContentBlock::Text(t) => Some(t.text.as_str()),
1154                        _ => None,
1155                    })
1156                    .collect();
1157
1158                if texts.is_empty() {
1159                    None
1160                } else {
1161                    Some(texts.join(""))
1162                }
1163            }
1164            _ => None,
1165        }
1166    }
1167
1168    /// Get the assistant message if this is one.
1169    ///
1170    /// # Example
1171    /// ```
1172    /// use claude_codes::ClaudeOutput;
1173    ///
1174    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1175    ///     "model":"claude-3","content":[]},"session_id":"abc"}"#;
1176    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1177    ///
1178    /// if let Some(assistant) = output.as_assistant() {
1179    ///     assert_eq!(assistant.message.model, "claude-3");
1180    /// }
1181    /// ```
1182    pub fn as_assistant(&self) -> Option<&AssistantMessage> {
1183        match self {
1184            ClaudeOutput::Assistant(ass) => Some(ass),
1185            _ => None,
1186        }
1187    }
1188
1189    /// Get the result message if this is one.
1190    ///
1191    /// # Example
1192    /// ```
1193    /// use claude_codes::ClaudeOutput;
1194    ///
1195    /// let json = r#"{"type":"result","subtype":"success","is_error":false,
1196    ///     "duration_ms":100,"duration_api_ms":200,"num_turns":1,
1197    ///     "session_id":"abc","total_cost_usd":0.01}"#;
1198    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1199    ///
1200    /// if let Some(result) = output.as_result() {
1201    ///     assert!(!result.is_error);
1202    /// }
1203    /// ```
1204    pub fn as_result(&self) -> Option<&ResultMessage> {
1205        match self {
1206            ClaudeOutput::Result(res) => Some(res),
1207            _ => None,
1208        }
1209    }
1210
1211    /// Get the system message if this is one.
1212    pub fn as_system(&self) -> Option<&SystemMessage> {
1213        match self {
1214            ClaudeOutput::System(sys) => Some(sys),
1215            _ => None,
1216        }
1217    }
1218
1219    /// Parse a JSON string, handling potential ANSI escape codes and other prefixes
1220    /// This method will:
1221    /// 1. First try to parse as-is
1222    /// 2. If that fails, trim until it finds a '{' and try again
1223    pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
1224        // First try to parse as-is
1225        match Self::parse_json(s) {
1226            Ok(output) => Ok(output),
1227            Err(first_error) => {
1228                // If that fails, look for the first '{' character
1229                if let Some(json_start) = s.find('{') {
1230                    let trimmed = &s[json_start..];
1231                    match Self::parse_json(trimmed) {
1232                        Ok(output) => Ok(output),
1233                        Err(_) => {
1234                            // Return the original error if both attempts fail
1235                            Err(first_error)
1236                        }
1237                    }
1238                } else {
1239                    Err(first_error)
1240                }
1241            }
1242        }
1243    }
1244
1245    /// Parse a JSON string, returning ParseError with raw JSON if it doesn't match our types
1246    pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
1247        // First try to parse as a Value
1248        let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
1249            raw_json: Value::String(s.to_string()),
1250            error_message: format!("Invalid JSON: {}", e),
1251        })?;
1252
1253        // Then try to parse that Value as ClaudeOutput
1254        serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
1255            raw_json: value,
1256            error_message: e.to_string(),
1257        })
1258    }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::*;
1264
1265    #[test]
1266    fn test_serialize_user_message() {
1267        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1268        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
1269        let json = serde_json::to_string(&input).unwrap();
1270        assert!(json.contains("\"type\":\"user\""));
1271        assert!(json.contains("\"role\":\"user\""));
1272        assert!(json.contains("\"text\":\"Hello, Claude!\""));
1273        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
1274    }
1275
1276    #[test]
1277    fn test_deserialize_assistant_message() {
1278        let json = r#"{
1279            "type": "assistant",
1280            "message": {
1281                "id": "msg_123",
1282                "role": "assistant",
1283                "model": "claude-3-sonnet",
1284                "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1285            },
1286            "session_id": "123"
1287        }"#;
1288
1289        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1290        assert!(output.is_assistant_message());
1291    }
1292
1293    #[test]
1294    fn test_deserialize_result_message() {
1295        let json = r#"{
1296            "type": "result",
1297            "subtype": "success",
1298            "is_error": false,
1299            "duration_ms": 100,
1300            "duration_api_ms": 200,
1301            "num_turns": 1,
1302            "result": "Done",
1303            "session_id": "123",
1304            "total_cost_usd": 0.01,
1305            "permission_denials": []
1306        }"#;
1307
1308        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1309        assert!(!output.is_error());
1310    }
1311
1312    #[test]
1313    fn test_deserialize_result_with_permission_denials() {
1314        let json = r#"{
1315            "type": "result",
1316            "subtype": "success",
1317            "is_error": false,
1318            "duration_ms": 100,
1319            "duration_api_ms": 200,
1320            "num_turns": 2,
1321            "result": "Done",
1322            "session_id": "123",
1323            "total_cost_usd": 0.01,
1324            "permission_denials": [
1325                {
1326                    "tool_name": "Bash",
1327                    "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1328                    "tool_use_id": "toolu_123"
1329                }
1330            ]
1331        }"#;
1332
1333        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1334        if let ClaudeOutput::Result(result) = output {
1335            assert_eq!(result.permission_denials.len(), 1);
1336            assert_eq!(result.permission_denials[0].tool_name, "Bash");
1337            assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1338            assert_eq!(
1339                result.permission_denials[0]
1340                    .tool_input
1341                    .get("command")
1342                    .unwrap(),
1343                "rm -rf /"
1344            );
1345        } else {
1346            panic!("Expected Result");
1347        }
1348    }
1349
1350    #[test]
1351    fn test_permission_denial_roundtrip() {
1352        let denial = PermissionDenial {
1353            tool_name: "Write".to_string(),
1354            tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1355            tool_use_id: "toolu_456".to_string(),
1356        };
1357
1358        let json = serde_json::to_string(&denial).unwrap();
1359        assert!(json.contains("\"tool_name\":\"Write\""));
1360        assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1361        assert!(json.contains("/etc/passwd"));
1362
1363        let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1364        assert_eq!(parsed, denial);
1365    }
1366
1367    // ============================================================================
1368    // Control Protocol Tests
1369    // ============================================================================
1370
1371    #[test]
1372    fn test_deserialize_control_request_can_use_tool() {
1373        let json = r#"{
1374            "type": "control_request",
1375            "request_id": "perm-abc123",
1376            "request": {
1377                "subtype": "can_use_tool",
1378                "tool_name": "Write",
1379                "input": {
1380                    "file_path": "/home/user/hello.py",
1381                    "content": "print('hello')"
1382                },
1383                "permission_suggestions": [],
1384                "blocked_path": null
1385            }
1386        }"#;
1387
1388        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1389        assert!(output.is_control_request());
1390
1391        if let ClaudeOutput::ControlRequest(req) = output {
1392            assert_eq!(req.request_id, "perm-abc123");
1393            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1394                assert_eq!(perm_req.tool_name, "Write");
1395                assert_eq!(
1396                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1397                    "/home/user/hello.py"
1398                );
1399            } else {
1400                panic!("Expected CanUseTool payload");
1401            }
1402        } else {
1403            panic!("Expected ControlRequest");
1404        }
1405    }
1406
1407    #[test]
1408    fn test_deserialize_control_request_edit_tool_real() {
1409        // Real production message from Claude CLI
1410        let json = r#"{"type":"control_request","request_id":"f3cf357c-17d6-4eca-b498-dd17c7ac43dd","request":{"subtype":"can_use_tool","tool_name":"Edit","input":{"file_path":"/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs","old_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n    println!(\n        \"  {} Run: {} to re-authenticate\",\n        \"→\".bright_blue(),\n        \"claude-portal logout && claude-portal login\".bright_cyan()\n    );\n}","new_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n    println!(\n        \"  {} Run: {} to re-authenticate\",\n        \"→\".bright_blue(),\n        \"claude-portal --reauth\".bright_cyan()\n    );\n}","replace_all":false},"permission_suggestions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}],"tool_use_id":"toolu_015BDGtNiqNrRSJSDrWXNckW"}}"#;
1411
1412        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1413        assert!(output.is_control_request());
1414        assert_eq!(output.message_type(), "control_request");
1415
1416        if let ClaudeOutput::ControlRequest(req) = output {
1417            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1418            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1419                assert_eq!(perm_req.tool_name, "Edit");
1420                // Verify input contains the expected Edit fields
1421                assert_eq!(
1422                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1423                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1424                );
1425                assert!(perm_req.input.get("old_string").is_some());
1426                assert!(perm_req.input.get("new_string").is_some());
1427                assert!(!perm_req
1428                    .input
1429                    .get("replace_all")
1430                    .unwrap()
1431                    .as_bool()
1432                    .unwrap());
1433            } else {
1434                panic!("Expected CanUseTool payload");
1435            }
1436        } else {
1437            panic!("Expected ControlRequest");
1438        }
1439    }
1440
1441    #[test]
1442    fn test_tool_permission_request_allow() {
1443        let req = ToolPermissionRequest {
1444            tool_name: "Read".to_string(),
1445            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1446            permission_suggestions: vec![],
1447            blocked_path: None,
1448        };
1449
1450        let response = req.allow("req-123");
1451        let message: ControlResponseMessage = response.into();
1452
1453        let json = serde_json::to_string(&message).unwrap();
1454        assert!(json.contains("\"type\":\"control_response\""));
1455        assert!(json.contains("\"subtype\":\"success\""));
1456        assert!(json.contains("\"request_id\":\"req-123\""));
1457        assert!(json.contains("\"behavior\":\"allow\""));
1458        assert!(json.contains("\"updatedInput\""));
1459    }
1460
1461    #[test]
1462    fn test_tool_permission_request_allow_with_modified_input() {
1463        let req = ToolPermissionRequest {
1464            tool_name: "Write".to_string(),
1465            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1466            permission_suggestions: vec![],
1467            blocked_path: None,
1468        };
1469
1470        let modified_input = serde_json::json!({
1471            "file_path": "/tmp/safe/passwd",
1472            "content": "test"
1473        });
1474        let response = req.allow_with(modified_input, "req-456");
1475        let message: ControlResponseMessage = response.into();
1476
1477        let json = serde_json::to_string(&message).unwrap();
1478        assert!(json.contains("/tmp/safe/passwd"));
1479        assert!(!json.contains("/etc/passwd"));
1480    }
1481
1482    #[test]
1483    fn test_tool_permission_request_deny() {
1484        let req = ToolPermissionRequest {
1485            tool_name: "Bash".to_string(),
1486            input: serde_json::json!({"command": "sudo rm -rf /"}),
1487            permission_suggestions: vec![],
1488            blocked_path: None,
1489        };
1490
1491        let response = req.deny("Dangerous command blocked", "req-789");
1492        let message: ControlResponseMessage = response.into();
1493
1494        let json = serde_json::to_string(&message).unwrap();
1495        assert!(json.contains("\"behavior\":\"deny\""));
1496        assert!(json.contains("Dangerous command blocked"));
1497        assert!(!json.contains("\"interrupt\":true"));
1498    }
1499
1500    #[test]
1501    fn test_tool_permission_request_deny_and_stop() {
1502        let req = ToolPermissionRequest {
1503            tool_name: "Bash".to_string(),
1504            input: serde_json::json!({"command": "rm -rf /"}),
1505            permission_suggestions: vec![],
1506            blocked_path: None,
1507        };
1508
1509        let response = req.deny_and_stop("Security violation", "req-000");
1510        let message: ControlResponseMessage = response.into();
1511
1512        let json = serde_json::to_string(&message).unwrap();
1513        assert!(json.contains("\"behavior\":\"deny\""));
1514        assert!(json.contains("\"interrupt\":true"));
1515    }
1516
1517    #[test]
1518    fn test_permission_result_serialization() {
1519        // Test allow
1520        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1521        let json = serde_json::to_string(&allow).unwrap();
1522        assert!(json.contains("\"behavior\":\"allow\""));
1523        assert!(json.contains("\"updatedInput\""));
1524
1525        // Test deny
1526        let deny = PermissionResult::deny("Not allowed");
1527        let json = serde_json::to_string(&deny).unwrap();
1528        assert!(json.contains("\"behavior\":\"deny\""));
1529        assert!(json.contains("\"message\":\"Not allowed\""));
1530        assert!(!json.contains("\"interrupt\""));
1531
1532        // Test deny with interrupt
1533        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1534        let json = serde_json::to_string(&deny_stop).unwrap();
1535        assert!(json.contains("\"interrupt\":true"));
1536    }
1537
1538    #[test]
1539    fn test_control_request_message_initialize() {
1540        let init = ControlRequestMessage::initialize("init-1");
1541
1542        let json = serde_json::to_string(&init).unwrap();
1543        assert!(json.contains("\"type\":\"control_request\""));
1544        assert!(json.contains("\"request_id\":\"init-1\""));
1545        assert!(json.contains("\"subtype\":\"initialize\""));
1546    }
1547
1548    #[test]
1549    fn test_control_response_error() {
1550        let response = ControlResponse::error("req-err", "Something went wrong");
1551        let message: ControlResponseMessage = response.into();
1552
1553        let json = serde_json::to_string(&message).unwrap();
1554        assert!(json.contains("\"subtype\":\"error\""));
1555        assert!(json.contains("\"error\":\"Something went wrong\""));
1556    }
1557
1558    #[test]
1559    fn test_roundtrip_control_request() {
1560        // Test that we can serialize and deserialize control requests
1561        let original_json = r#"{
1562            "type": "control_request",
1563            "request_id": "test-123",
1564            "request": {
1565                "subtype": "can_use_tool",
1566                "tool_name": "Bash",
1567                "input": {"command": "ls -la"},
1568                "permission_suggestions": []
1569            }
1570        }"#;
1571
1572        // Parse as ClaudeOutput
1573        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1574
1575        // Serialize back and verify key parts are present
1576        let reserialized = serde_json::to_string(&output).unwrap();
1577        assert!(reserialized.contains("control_request"));
1578        assert!(reserialized.contains("test-123"));
1579        assert!(reserialized.contains("Bash"));
1580    }
1581
1582    #[test]
1583    fn test_permission_suggestions_parsing() {
1584        // Test that permission_suggestions deserialize correctly with real protocol format
1585        let json = r#"{
1586            "type": "control_request",
1587            "request_id": "perm-456",
1588            "request": {
1589                "subtype": "can_use_tool",
1590                "tool_name": "Bash",
1591                "input": {"command": "npm test"},
1592                "permission_suggestions": [
1593                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1594                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1595                ]
1596            }
1597        }"#;
1598
1599        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1600        if let ClaudeOutput::ControlRequest(req) = output {
1601            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1602                assert_eq!(perm_req.permission_suggestions.len(), 2);
1603                assert_eq!(
1604                    perm_req.permission_suggestions[0].suggestion_type,
1605                    "setMode"
1606                );
1607                assert_eq!(
1608                    perm_req.permission_suggestions[0].mode,
1609                    Some("acceptEdits".to_string())
1610                );
1611                assert_eq!(perm_req.permission_suggestions[0].destination, "session");
1612                assert_eq!(
1613                    perm_req.permission_suggestions[1].suggestion_type,
1614                    "setMode"
1615                );
1616                assert_eq!(
1617                    perm_req.permission_suggestions[1].mode,
1618                    Some("bypassPermissions".to_string())
1619                );
1620                assert_eq!(perm_req.permission_suggestions[1].destination, "project");
1621            } else {
1622                panic!("Expected CanUseTool payload");
1623            }
1624        } else {
1625            panic!("Expected ControlRequest");
1626        }
1627    }
1628
1629    #[test]
1630    fn test_permission_suggestion_set_mode_roundtrip() {
1631        let suggestion = PermissionSuggestion {
1632            suggestion_type: "setMode".to_string(),
1633            destination: "session".to_string(),
1634            mode: Some("acceptEdits".to_string()),
1635            behavior: None,
1636            rules: None,
1637        };
1638
1639        let json = serde_json::to_string(&suggestion).unwrap();
1640        assert!(json.contains("\"type\":\"setMode\""));
1641        assert!(json.contains("\"mode\":\"acceptEdits\""));
1642        assert!(json.contains("\"destination\":\"session\""));
1643        assert!(!json.contains("\"behavior\""));
1644        assert!(!json.contains("\"rules\""));
1645
1646        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1647        assert_eq!(parsed, suggestion);
1648    }
1649
1650    #[test]
1651    fn test_permission_suggestion_add_rules_roundtrip() {
1652        let suggestion = PermissionSuggestion {
1653            suggestion_type: "addRules".to_string(),
1654            destination: "session".to_string(),
1655            mode: None,
1656            behavior: Some("allow".to_string()),
1657            rules: Some(vec![serde_json::json!({
1658                "toolName": "Read",
1659                "ruleContent": "//tmp/**"
1660            })]),
1661        };
1662
1663        let json = serde_json::to_string(&suggestion).unwrap();
1664        assert!(json.contains("\"type\":\"addRules\""));
1665        assert!(json.contains("\"behavior\":\"allow\""));
1666        assert!(json.contains("\"destination\":\"session\""));
1667        assert!(json.contains("\"rules\""));
1668        assert!(json.contains("\"toolName\":\"Read\""));
1669        assert!(!json.contains("\"mode\""));
1670
1671        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1672        assert_eq!(parsed, suggestion);
1673    }
1674
1675    #[test]
1676    fn test_permission_suggestion_add_rules_from_real_json() {
1677        // Real production message from Claude CLI
1678        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1679
1680        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1681        assert_eq!(parsed.suggestion_type, "addRules");
1682        assert_eq!(parsed.destination, "session");
1683        assert_eq!(parsed.behavior, Some("allow".to_string()));
1684        assert!(parsed.rules.is_some());
1685        assert!(parsed.mode.is_none());
1686    }
1687
1688    // ============================================================================
1689    // System Message Subtype Tests
1690    // ============================================================================
1691
1692    #[test]
1693    fn test_system_message_init() {
1694        let json = r#"{
1695            "type": "system",
1696            "subtype": "init",
1697            "session_id": "test-session-123",
1698            "cwd": "/home/user/project",
1699            "model": "claude-sonnet-4",
1700            "tools": ["Bash", "Read", "Write"],
1701            "mcp_servers": []
1702        }"#;
1703
1704        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1705        if let ClaudeOutput::System(sys) = output {
1706            assert!(sys.is_init());
1707            assert!(!sys.is_status());
1708            assert!(!sys.is_compact_boundary());
1709
1710            let init = sys.as_init().expect("Should parse as init");
1711            assert_eq!(init.session_id, "test-session-123");
1712            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
1713            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
1714            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
1715        } else {
1716            panic!("Expected System message");
1717        }
1718    }
1719
1720    #[test]
1721    fn test_system_message_status() {
1722        let json = r#"{
1723            "type": "system",
1724            "subtype": "status",
1725            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1726            "status": "compacting",
1727            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
1728        }"#;
1729
1730        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1731        if let ClaudeOutput::System(sys) = output {
1732            assert!(sys.is_status());
1733            assert!(!sys.is_init());
1734
1735            let status = sys.as_status().expect("Should parse as status");
1736            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1737            assert_eq!(status.status, Some("compacting".to_string()));
1738            assert_eq!(
1739                status.uuid,
1740                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
1741            );
1742        } else {
1743            panic!("Expected System message");
1744        }
1745    }
1746
1747    #[test]
1748    fn test_system_message_status_null() {
1749        let json = r#"{
1750            "type": "system",
1751            "subtype": "status",
1752            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1753            "status": null,
1754            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
1755        }"#;
1756
1757        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1758        if let ClaudeOutput::System(sys) = output {
1759            let status = sys.as_status().expect("Should parse as status");
1760            assert_eq!(status.status, None);
1761        } else {
1762            panic!("Expected System message");
1763        }
1764    }
1765
1766    #[test]
1767    fn test_system_message_compact_boundary() {
1768        let json = r#"{
1769            "type": "system",
1770            "subtype": "compact_boundary",
1771            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1772            "compact_metadata": {
1773                "pre_tokens": 155285,
1774                "trigger": "auto"
1775            },
1776            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1777        }"#;
1778
1779        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1780        if let ClaudeOutput::System(sys) = output {
1781            assert!(sys.is_compact_boundary());
1782            assert!(!sys.is_init());
1783            assert!(!sys.is_status());
1784
1785            let compact = sys
1786                .as_compact_boundary()
1787                .expect("Should parse as compact_boundary");
1788            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1789            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1790            assert_eq!(compact.compact_metadata.trigger, "auto");
1791        } else {
1792            panic!("Expected System message");
1793        }
1794    }
1795
1796    // ============================================================================
1797    // Helper Method Tests
1798    // ============================================================================
1799
1800    #[test]
1801    fn test_is_system_init() {
1802        let init_json = r#"{
1803            "type": "system",
1804            "subtype": "init",
1805            "session_id": "test-session"
1806        }"#;
1807        let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
1808        assert!(output.is_system_init());
1809
1810        let status_json = r#"{
1811            "type": "system",
1812            "subtype": "status",
1813            "session_id": "test-session"
1814        }"#;
1815        let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
1816        assert!(!output.is_system_init());
1817    }
1818
1819    #[test]
1820    fn test_session_id() {
1821        // Result message
1822        let result_json = r#"{
1823            "type": "result",
1824            "subtype": "success",
1825            "is_error": false,
1826            "duration_ms": 100,
1827            "duration_api_ms": 200,
1828            "num_turns": 1,
1829            "session_id": "result-session",
1830            "total_cost_usd": 0.01
1831        }"#;
1832        let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1833        assert_eq!(output.session_id(), Some("result-session"));
1834
1835        // Assistant message
1836        let assistant_json = r#"{
1837            "type": "assistant",
1838            "message": {
1839                "id": "msg_1",
1840                "role": "assistant",
1841                "model": "claude-3",
1842                "content": []
1843            },
1844            "session_id": "assistant-session"
1845        }"#;
1846        let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1847        assert_eq!(output.session_id(), Some("assistant-session"));
1848
1849        // System message
1850        let system_json = r#"{
1851            "type": "system",
1852            "subtype": "init",
1853            "session_id": "system-session"
1854        }"#;
1855        let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
1856        assert_eq!(output.session_id(), Some("system-session"));
1857    }
1858
1859    #[test]
1860    fn test_as_tool_use() {
1861        let json = r#"{
1862            "type": "assistant",
1863            "message": {
1864                "id": "msg_1",
1865                "role": "assistant",
1866                "model": "claude-3",
1867                "content": [
1868                    {"type": "text", "text": "Let me run that command."},
1869                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
1870                    {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
1871                ]
1872            },
1873            "session_id": "abc"
1874        }"#;
1875        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1876
1877        // Find Bash tool
1878        let bash = output.as_tool_use("Bash");
1879        assert!(bash.is_some());
1880        assert_eq!(bash.unwrap().id, "tu_1");
1881
1882        // Find Read tool
1883        let read = output.as_tool_use("Read");
1884        assert!(read.is_some());
1885        assert_eq!(read.unwrap().id, "tu_2");
1886
1887        // Non-existent tool
1888        assert!(output.as_tool_use("Write").is_none());
1889
1890        // Not an assistant message
1891        let result_json = r#"{
1892            "type": "result",
1893            "subtype": "success",
1894            "is_error": false,
1895            "duration_ms": 100,
1896            "duration_api_ms": 200,
1897            "num_turns": 1,
1898            "session_id": "abc",
1899            "total_cost_usd": 0.01
1900        }"#;
1901        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1902        assert!(result.as_tool_use("Bash").is_none());
1903    }
1904
1905    #[test]
1906    fn test_tool_uses() {
1907        let json = r#"{
1908            "type": "assistant",
1909            "message": {
1910                "id": "msg_1",
1911                "role": "assistant",
1912                "model": "claude-3",
1913                "content": [
1914                    {"type": "text", "text": "Running commands..."},
1915                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
1916                    {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
1917                    {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
1918                ]
1919            },
1920            "session_id": "abc"
1921        }"#;
1922        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1923
1924        let tools: Vec<_> = output.tool_uses().collect();
1925        assert_eq!(tools.len(), 3);
1926        assert_eq!(tools[0].name, "Bash");
1927        assert_eq!(tools[1].name, "Read");
1928        assert_eq!(tools[2].name, "Write");
1929    }
1930
1931    #[test]
1932    fn test_text_content() {
1933        // Single text block
1934        let json = r#"{
1935            "type": "assistant",
1936            "message": {
1937                "id": "msg_1",
1938                "role": "assistant",
1939                "model": "claude-3",
1940                "content": [{"type": "text", "text": "Hello, world!"}]
1941            },
1942            "session_id": "abc"
1943        }"#;
1944        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1945        assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1946
1947        // Multiple text blocks
1948        let json = r#"{
1949            "type": "assistant",
1950            "message": {
1951                "id": "msg_1",
1952                "role": "assistant",
1953                "model": "claude-3",
1954                "content": [
1955                    {"type": "text", "text": "Hello, "},
1956                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
1957                    {"type": "text", "text": "world!"}
1958                ]
1959            },
1960            "session_id": "abc"
1961        }"#;
1962        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1963        assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1964
1965        // No text blocks
1966        let json = r#"{
1967            "type": "assistant",
1968            "message": {
1969                "id": "msg_1",
1970                "role": "assistant",
1971                "model": "claude-3",
1972                "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
1973            },
1974            "session_id": "abc"
1975        }"#;
1976        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1977        assert_eq!(output.text_content(), None);
1978
1979        // Not an assistant message
1980        let json = r#"{
1981            "type": "result",
1982            "subtype": "success",
1983            "is_error": false,
1984            "duration_ms": 100,
1985            "duration_api_ms": 200,
1986            "num_turns": 1,
1987            "session_id": "abc",
1988            "total_cost_usd": 0.01
1989        }"#;
1990        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1991        assert_eq!(output.text_content(), None);
1992    }
1993
1994    #[test]
1995    fn test_as_assistant() {
1996        let json = r#"{
1997            "type": "assistant",
1998            "message": {
1999                "id": "msg_1",
2000                "role": "assistant",
2001                "model": "claude-sonnet-4",
2002                "content": []
2003            },
2004            "session_id": "abc"
2005        }"#;
2006        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2007
2008        let assistant = output.as_assistant();
2009        assert!(assistant.is_some());
2010        assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
2011
2012        // Not an assistant
2013        let result_json = r#"{
2014            "type": "result",
2015            "subtype": "success",
2016            "is_error": false,
2017            "duration_ms": 100,
2018            "duration_api_ms": 200,
2019            "num_turns": 1,
2020            "session_id": "abc",
2021            "total_cost_usd": 0.01
2022        }"#;
2023        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2024        assert!(result.as_assistant().is_none());
2025    }
2026
2027    #[test]
2028    fn test_as_result() {
2029        let json = r#"{
2030            "type": "result",
2031            "subtype": "success",
2032            "is_error": false,
2033            "duration_ms": 100,
2034            "duration_api_ms": 200,
2035            "num_turns": 5,
2036            "session_id": "abc",
2037            "total_cost_usd": 0.05
2038        }"#;
2039        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2040
2041        let result = output.as_result();
2042        assert!(result.is_some());
2043        assert_eq!(result.unwrap().num_turns, 5);
2044        assert_eq!(result.unwrap().total_cost_usd, 0.05);
2045
2046        // Not a result
2047        let assistant_json = r#"{
2048            "type": "assistant",
2049            "message": {
2050                "id": "msg_1",
2051                "role": "assistant",
2052                "model": "claude-3",
2053                "content": []
2054            },
2055            "session_id": "abc"
2056        }"#;
2057        let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
2058        assert!(assistant.as_result().is_none());
2059    }
2060
2061    #[test]
2062    fn test_as_system() {
2063        let json = r#"{
2064            "type": "system",
2065            "subtype": "init",
2066            "session_id": "abc",
2067            "model": "claude-3"
2068        }"#;
2069        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2070
2071        let system = output.as_system();
2072        assert!(system.is_some());
2073        assert!(system.unwrap().is_init());
2074
2075        // Not a system message
2076        let result_json = r#"{
2077            "type": "result",
2078            "subtype": "success",
2079            "is_error": false,
2080            "duration_ms": 100,
2081            "duration_api_ms": 200,
2082            "num_turns": 1,
2083            "session_id": "abc",
2084            "total_cost_usd": 0.01
2085        }"#;
2086        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2087        assert!(result.as_system().is_none());
2088    }
2089
2090    // ============================================================================
2091    // ResultMessage Errors Field Tests
2092    // ============================================================================
2093
2094    #[test]
2095    fn test_deserialize_result_message_with_errors() {
2096        let json = r#"{
2097            "type": "result",
2098            "subtype": "error_during_execution",
2099            "duration_ms": 0,
2100            "duration_api_ms": 0,
2101            "is_error": true,
2102            "num_turns": 0,
2103            "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2104            "total_cost_usd": 0,
2105            "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2106            "permission_denials": []
2107        }"#;
2108
2109        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2110        assert!(output.is_error());
2111
2112        if let ClaudeOutput::Result(res) = output {
2113            assert!(res.is_error);
2114            assert_eq!(res.errors.len(), 1);
2115            assert!(res.errors[0].contains("No conversation found"));
2116        } else {
2117            panic!("Expected Result message");
2118        }
2119    }
2120
2121    #[test]
2122    fn test_deserialize_result_message_errors_defaults_empty() {
2123        // Test that errors field defaults to empty Vec when not present
2124        let json = r#"{
2125            "type": "result",
2126            "subtype": "success",
2127            "is_error": false,
2128            "duration_ms": 100,
2129            "duration_api_ms": 200,
2130            "num_turns": 1,
2131            "session_id": "123",
2132            "total_cost_usd": 0.01
2133        }"#;
2134
2135        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2136        if let ClaudeOutput::Result(res) = output {
2137            assert!(res.errors.is_empty());
2138        } else {
2139            panic!("Expected Result message");
2140        }
2141    }
2142
2143    #[test]
2144    fn test_result_message_errors_roundtrip() {
2145        let json = r#"{
2146            "type": "result",
2147            "subtype": "error_during_execution",
2148            "is_error": true,
2149            "duration_ms": 0,
2150            "duration_api_ms": 0,
2151            "num_turns": 0,
2152            "session_id": "test-session",
2153            "total_cost_usd": 0.0,
2154            "errors": ["Error 1", "Error 2"]
2155        }"#;
2156
2157        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2158        let reserialized = serde_json::to_string(&output).unwrap();
2159
2160        // Verify the errors are preserved
2161        assert!(reserialized.contains("Error 1"));
2162        assert!(reserialized.contains("Error 2"));
2163    }
2164}