Skip to main content

claude_codes/
tool_inputs.rs

1//! Typed tool input definitions for Claude Code tools.
2//!
3//! This module provides strongly-typed structs for the input parameters of each
4//! Claude Code tool. Using these types instead of raw `serde_json::Value` provides:
5//!
6//! - Compile-time type checking
7//! - IDE autocompletion and documentation
8//! - Self-documenting API
9//!
10//! # Example
11//!
12//! ```
13//! use claude_codes::{ToolInput, BashInput};
14//!
15//! // Parse a tool input from JSON
16//! let json = serde_json::json!({
17//!     "command": "ls -la",
18//!     "description": "List files in current directory"
19//! });
20//!
21//! let input: ToolInput = serde_json::from_value(json).unwrap();
22//! if let ToolInput::Bash(bash) = input {
23//!     assert_eq!(bash.command, "ls -la");
24//! }
25//! ```
26
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::collections::HashMap;
30
31// ============================================================================
32// Individual Tool Input Structs
33// ============================================================================
34
35/// Input for the Bash tool - executes shell commands.
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct BashInput {
38    /// The bash command to execute (required)
39    pub command: String,
40
41    /// Human-readable description of what the command does
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub description: Option<String>,
44
45    /// Timeout in milliseconds (max 600000)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub timeout: Option<u64>,
48
49    /// Whether to run the command in the background
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub run_in_background: Option<bool>,
52}
53
54/// Input for the Read tool - reads file contents.
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub struct ReadInput {
57    /// The absolute path to the file to read
58    pub file_path: String,
59
60    /// The line number to start reading from (1-indexed)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub offset: Option<i64>,
63
64    /// The number of lines to read
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub limit: Option<i64>,
67}
68
69/// Input for the Write tool - writes content to a file.
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct WriteInput {
72    /// The absolute path to the file to write
73    pub file_path: String,
74
75    /// The content to write to the file
76    pub content: String,
77}
78
79/// Input for the Edit tool - performs string replacements in files.
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct EditInput {
82    /// The absolute path to the file to modify
83    pub file_path: String,
84
85    /// The text to replace
86    pub old_string: String,
87
88    /// The text to replace it with
89    pub new_string: String,
90
91    /// Replace all occurrences of old_string (default false)
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub replace_all: Option<bool>,
94}
95
96/// Input for the Glob tool - finds files matching a pattern.
97///
98/// The `deny_unknown_fields` attribute ensures Glob only matches exact
99/// Glob inputs and doesn't accidentally match Grep inputs (which share
100/// the `pattern` field but have additional Grep-specific fields).
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102#[serde(deny_unknown_fields)]
103pub struct GlobInput {
104    /// The glob pattern to match files against (e.g., "**/*.rs")
105    pub pattern: String,
106
107    /// The directory to search in (defaults to current working directory)
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub path: Option<String>,
110}
111
112/// Input for the Grep tool - searches file contents.
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
114pub struct GrepInput {
115    /// The regular expression pattern to search for
116    pub pattern: String,
117
118    /// File or directory to search in
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub path: Option<String>,
121
122    /// Glob pattern to filter files (e.g., "*.js")
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub glob: Option<String>,
125
126    /// File type to search (e.g., "js", "py", "rust")
127    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
128    pub file_type: Option<String>,
129
130    /// Case insensitive search
131    #[serde(rename = "-i", skip_serializing_if = "Option::is_none")]
132    pub case_insensitive: Option<bool>,
133
134    /// Show line numbers in output
135    #[serde(rename = "-n", skip_serializing_if = "Option::is_none")]
136    pub line_numbers: Option<bool>,
137
138    /// Number of lines to show after each match
139    #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
140    pub after_context: Option<u32>,
141
142    /// Number of lines to show before each match
143    #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
144    pub before_context: Option<u32>,
145
146    /// Number of lines to show before and after each match
147    #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
148    pub context: Option<u32>,
149
150    /// Output mode: "content", "files_with_matches", or "count"
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub output_mode: Option<String>,
153
154    /// Enable multiline mode
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub multiline: Option<bool>,
157
158    /// Limit output to first N lines/entries
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub head_limit: Option<u32>,
161
162    /// Skip first N lines/entries
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub offset: Option<u32>,
165}
166
167/// Input for the Task tool - launches subagents.
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub struct TaskInput {
170    /// A short (3-5 word) description of the task
171    pub description: String,
172
173    /// The task for the agent to perform
174    pub prompt: String,
175
176    /// The type of specialized agent to use
177    pub subagent_type: String,
178
179    /// Whether to run the agent in the background
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub run_in_background: Option<bool>,
182
183    /// Optional model to use for this agent
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub model: Option<String>,
186
187    /// Maximum number of agentic turns
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub max_turns: Option<u32>,
190
191    /// Optional agent ID to resume from
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub resume: Option<String>,
194}
195
196/// Input for the WebFetch tool - fetches and processes web content.
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198pub struct WebFetchInput {
199    /// The URL to fetch content from
200    pub url: String,
201
202    /// The prompt to run on the fetched content
203    pub prompt: String,
204}
205
206/// Input for the WebSearch tool - searches the web.
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
208pub struct WebSearchInput {
209    /// The search query to use
210    pub query: String,
211
212    /// Only include search results from these domains
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub allowed_domains: Option<Vec<String>>,
215
216    /// Never include search results from these domains
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub blocked_domains: Option<Vec<String>>,
219}
220
221/// Input for the TodoWrite tool - manages task lists.
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
223pub struct TodoWriteInput {
224    /// The updated todo list
225    pub todos: Vec<TodoItem>,
226}
227
228/// A single todo item in a task list.
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub struct TodoItem {
231    /// The task description (imperative form)
232    pub content: String,
233
234    /// Current status: "pending", "in_progress", or "completed"
235    pub status: String,
236
237    /// The present continuous form shown during execution
238    #[serde(rename = "activeForm")]
239    pub active_form: String,
240}
241
242/// Input for the AskUserQuestion tool - asks the user questions.
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244pub struct AskUserQuestionInput {
245    /// Questions to ask the user (1-4 questions)
246    pub questions: Vec<Question>,
247
248    /// User answers collected by the permission component
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub answers: Option<HashMap<String, String>>,
251
252    /// Optional metadata for tracking and analytics
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub metadata: Option<QuestionMetadata>,
255}
256
257/// A question to ask the user.
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259pub struct Question {
260    /// The complete question to ask the user
261    pub question: String,
262
263    /// Very short label displayed as a chip/tag (max 12 chars)
264    pub header: String,
265
266    /// The available choices for this question (2-4 options)
267    pub options: Vec<QuestionOption>,
268
269    /// Whether multiple options can be selected
270    #[serde(rename = "multiSelect", default)]
271    pub multi_select: bool,
272}
273
274/// An option for a question.
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276pub struct QuestionOption {
277    /// The display text for this option
278    pub label: String,
279
280    /// Explanation of what this option means
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub description: Option<String>,
283}
284
285/// Metadata for questions.
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct QuestionMetadata {
288    /// Optional identifier for the source of this question
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub source: Option<String>,
291}
292
293/// Input for the NotebookEdit tool - edits Jupyter notebooks.
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
295pub struct NotebookEditInput {
296    /// The absolute path to the Jupyter notebook file
297    pub notebook_path: String,
298
299    /// The new source for the cell
300    pub new_source: String,
301
302    /// The ID of the cell to edit
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub cell_id: Option<String>,
305
306    /// The type of the cell (code or markdown)
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub cell_type: Option<String>,
309
310    /// The type of edit to make (replace, insert, delete)
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub edit_mode: Option<String>,
313}
314
315/// Input for the TaskOutput tool - retrieves output from background tasks.
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct TaskOutputInput {
318    /// The task ID to get output from
319    pub task_id: String,
320
321    /// Whether to wait for completion (default true)
322    #[serde(default = "default_true")]
323    pub block: bool,
324
325    /// Max wait time in ms (default 30000, max 600000)
326    #[serde(default = "default_timeout")]
327    pub timeout: u64,
328}
329
330fn default_true() -> bool {
331    true
332}
333
334fn default_timeout() -> u64 {
335    30000
336}
337
338/// Input for the KillShell tool - kills a running background shell.
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340pub struct KillShellInput {
341    /// The ID of the background shell to kill
342    pub shell_id: String,
343}
344
345/// Input for the Skill tool - executes a skill.
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
347pub struct SkillInput {
348    /// The skill name (e.g., "commit", "review-pr")
349    pub skill: String,
350
351    /// Optional arguments for the skill
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub args: Option<String>,
354}
355
356/// Input for the EnterPlanMode tool - enters planning mode.
357///
358/// This is an empty struct as EnterPlanMode takes no parameters.
359/// The `deny_unknown_fields` attribute ensures this only matches
360/// empty JSON objects `{}`, not arbitrary JSON.
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
362#[serde(deny_unknown_fields)]
363pub struct EnterPlanModeInput {}
364
365/// Input for the ExitPlanMode tool - exits planning mode.
366///
367/// The `deny_unknown_fields` attribute ensures this only matches JSON objects
368/// that contain known fields (or are empty), not arbitrary JSON.
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
370#[serde(deny_unknown_fields)]
371pub struct ExitPlanModeInput {
372    /// Prompt-based permissions needed to implement the plan
373    #[serde(rename = "allowedPrompts", skip_serializing_if = "Option::is_none")]
374    pub allowed_prompts: Option<Vec<AllowedPrompt>>,
375
376    /// Whether to push the plan to a remote Claude.ai session
377    #[serde(rename = "pushToRemote", skip_serializing_if = "Option::is_none")]
378    pub push_to_remote: Option<bool>,
379
380    /// The remote session ID if pushed to remote
381    #[serde(rename = "remoteSessionId", skip_serializing_if = "Option::is_none")]
382    pub remote_session_id: Option<String>,
383
384    /// The remote session URL if pushed to remote
385    #[serde(rename = "remoteSessionUrl", skip_serializing_if = "Option::is_none")]
386    pub remote_session_url: Option<String>,
387
388    /// The remote session title if pushed to remote
389    #[serde(rename = "remoteSessionTitle", skip_serializing_if = "Option::is_none")]
390    pub remote_session_title: Option<String>,
391
392    /// The plan content from plan mode
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub plan: Option<String>,
395}
396
397/// An allowed prompt permission for plan mode.
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
399pub struct AllowedPrompt {
400    /// The tool this prompt applies to
401    pub tool: String,
402
403    /// Semantic description of the action
404    pub prompt: String,
405}
406
407// ============================================================================
408// ToolInput Enum - Unified type for all tool inputs
409// ============================================================================
410
411/// Unified enum representing input for any Claude Code tool.
412///
413/// This enum uses `#[serde(untagged)]` to automatically deserialize based on
414/// the structure of the JSON. The `Unknown` variant serves as a fallback for:
415/// - New tools added in future Claude CLI versions
416/// - Custom MCP tools provided by users
417/// - Any tool input that doesn't match known schemas
418///
419/// # Example
420///
421/// ```
422/// use claude_codes::ToolInput;
423///
424/// // Known tool - deserializes to specific variant
425/// let bash_json = serde_json::json!({"command": "ls"});
426/// let input: ToolInput = serde_json::from_value(bash_json).unwrap();
427/// assert!(matches!(input, ToolInput::Bash(_)));
428///
429/// // Unknown tool - falls back to Unknown variant
430/// let custom_json = serde_json::json!({"custom_field": "value"});
431/// let input: ToolInput = serde_json::from_value(custom_json).unwrap();
432/// assert!(matches!(input, ToolInput::Unknown(_)));
433/// ```
434///
435/// # Note on Ordering
436///
437/// The variants are ordered from most specific (most required fields) to least
438/// specific to ensure correct deserialization with `#[serde(untagged)]`.
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
440#[serde(untagged)]
441pub enum ToolInput {
442    /// Edit tool - has unique field combination (file_path, old_string, new_string)
443    Edit(EditInput),
444
445    /// Write tool - file_path + content
446    Write(WriteInput),
447
448    /// AskUserQuestion tool - has questions array
449    AskUserQuestion(AskUserQuestionInput),
450
451    /// TodoWrite tool - has todos array
452    TodoWrite(TodoWriteInput),
453
454    /// Task tool - description + prompt + subagent_type
455    Task(TaskInput),
456
457    /// NotebookEdit tool - notebook_path + new_source
458    NotebookEdit(NotebookEditInput),
459
460    /// WebFetch tool - url + prompt
461    WebFetch(WebFetchInput),
462
463    /// TaskOutput tool - task_id + block + timeout
464    TaskOutput(TaskOutputInput),
465
466    /// Bash tool - has command field
467    Bash(BashInput),
468
469    /// Read tool - has file_path
470    Read(ReadInput),
471
472    /// Glob tool - has pattern field (with deny_unknown_fields, must come before Grep)
473    Glob(GlobInput),
474
475    /// Grep tool - has pattern field plus many optional fields
476    Grep(GrepInput),
477
478    /// WebSearch tool - has query field
479    WebSearch(WebSearchInput),
480
481    /// KillShell tool - has shell_id
482    KillShell(KillShellInput),
483
484    /// Skill tool - has skill field
485    Skill(SkillInput),
486
487    /// ExitPlanMode tool
488    ExitPlanMode(ExitPlanModeInput),
489
490    /// EnterPlanMode tool (empty input)
491    EnterPlanMode(EnterPlanModeInput),
492
493    /// Unknown tool input - fallback for custom/new tools
494    ///
495    /// This variant captures any tool input that doesn't match the known schemas.
496    /// Use this for:
497    /// - MCP tools provided by users
498    /// - New tools in future Claude CLI versions
499    /// - Any custom tool integration
500    Unknown(Value),
501}
502
503impl ToolInput {
504    /// Returns the tool name if it can be determined from the input type.
505    ///
506    /// For `Unknown` variants, returns `None` since the tool name cannot be
507    /// determined from the input structure alone.
508    pub fn tool_name(&self) -> Option<&'static str> {
509        match self {
510            ToolInput::Bash(_) => Some("Bash"),
511            ToolInput::Read(_) => Some("Read"),
512            ToolInput::Write(_) => Some("Write"),
513            ToolInput::Edit(_) => Some("Edit"),
514            ToolInput::Glob(_) => Some("Glob"),
515            ToolInput::Grep(_) => Some("Grep"),
516            ToolInput::Task(_) => Some("Task"),
517            ToolInput::WebFetch(_) => Some("WebFetch"),
518            ToolInput::WebSearch(_) => Some("WebSearch"),
519            ToolInput::TodoWrite(_) => Some("TodoWrite"),
520            ToolInput::AskUserQuestion(_) => Some("AskUserQuestion"),
521            ToolInput::NotebookEdit(_) => Some("NotebookEdit"),
522            ToolInput::TaskOutput(_) => Some("TaskOutput"),
523            ToolInput::KillShell(_) => Some("KillShell"),
524            ToolInput::Skill(_) => Some("Skill"),
525            ToolInput::EnterPlanMode(_) => Some("EnterPlanMode"),
526            ToolInput::ExitPlanMode(_) => Some("ExitPlanMode"),
527            ToolInput::Unknown(_) => None,
528        }
529    }
530
531    /// Try to get the input as a Bash input.
532    pub fn as_bash(&self) -> Option<&BashInput> {
533        match self {
534            ToolInput::Bash(input) => Some(input),
535            _ => None,
536        }
537    }
538
539    /// Try to get the input as a Read input.
540    pub fn as_read(&self) -> Option<&ReadInput> {
541        match self {
542            ToolInput::Read(input) => Some(input),
543            _ => None,
544        }
545    }
546
547    /// Try to get the input as a Write input.
548    pub fn as_write(&self) -> Option<&WriteInput> {
549        match self {
550            ToolInput::Write(input) => Some(input),
551            _ => None,
552        }
553    }
554
555    /// Try to get the input as an Edit input.
556    pub fn as_edit(&self) -> Option<&EditInput> {
557        match self {
558            ToolInput::Edit(input) => Some(input),
559            _ => None,
560        }
561    }
562
563    /// Try to get the input as a Glob input.
564    pub fn as_glob(&self) -> Option<&GlobInput> {
565        match self {
566            ToolInput::Glob(input) => Some(input),
567            _ => None,
568        }
569    }
570
571    /// Try to get the input as a Grep input.
572    pub fn as_grep(&self) -> Option<&GrepInput> {
573        match self {
574            ToolInput::Grep(input) => Some(input),
575            _ => None,
576        }
577    }
578
579    /// Try to get the input as a Task input.
580    pub fn as_task(&self) -> Option<&TaskInput> {
581        match self {
582            ToolInput::Task(input) => Some(input),
583            _ => None,
584        }
585    }
586
587    /// Try to get the input as a WebFetch input.
588    pub fn as_web_fetch(&self) -> Option<&WebFetchInput> {
589        match self {
590            ToolInput::WebFetch(input) => Some(input),
591            _ => None,
592        }
593    }
594
595    /// Try to get the input as a WebSearch input.
596    pub fn as_web_search(&self) -> Option<&WebSearchInput> {
597        match self {
598            ToolInput::WebSearch(input) => Some(input),
599            _ => None,
600        }
601    }
602
603    /// Try to get the input as a TodoWrite input.
604    pub fn as_todo_write(&self) -> Option<&TodoWriteInput> {
605        match self {
606            ToolInput::TodoWrite(input) => Some(input),
607            _ => None,
608        }
609    }
610
611    /// Try to get the input as an AskUserQuestion input.
612    pub fn as_ask_user_question(&self) -> Option<&AskUserQuestionInput> {
613        match self {
614            ToolInput::AskUserQuestion(input) => Some(input),
615            _ => None,
616        }
617    }
618
619    /// Try to get the input as a NotebookEdit input.
620    pub fn as_notebook_edit(&self) -> Option<&NotebookEditInput> {
621        match self {
622            ToolInput::NotebookEdit(input) => Some(input),
623            _ => None,
624        }
625    }
626
627    /// Try to get the input as a TaskOutput input.
628    pub fn as_task_output(&self) -> Option<&TaskOutputInput> {
629        match self {
630            ToolInput::TaskOutput(input) => Some(input),
631            _ => None,
632        }
633    }
634
635    /// Try to get the input as a KillShell input.
636    pub fn as_kill_shell(&self) -> Option<&KillShellInput> {
637        match self {
638            ToolInput::KillShell(input) => Some(input),
639            _ => None,
640        }
641    }
642
643    /// Try to get the input as a Skill input.
644    pub fn as_skill(&self) -> Option<&SkillInput> {
645        match self {
646            ToolInput::Skill(input) => Some(input),
647            _ => None,
648        }
649    }
650
651    /// Try to get the input as an unknown Value.
652    pub fn as_unknown(&self) -> Option<&Value> {
653        match self {
654            ToolInput::Unknown(value) => Some(value),
655            _ => None,
656        }
657    }
658
659    /// Check if this is an unknown tool input.
660    pub fn is_unknown(&self) -> bool {
661        matches!(self, ToolInput::Unknown(_))
662    }
663}
664
665// ============================================================================
666// Conversion implementations
667// ============================================================================
668
669impl From<BashInput> for ToolInput {
670    fn from(input: BashInput) -> Self {
671        ToolInput::Bash(input)
672    }
673}
674
675impl From<ReadInput> for ToolInput {
676    fn from(input: ReadInput) -> Self {
677        ToolInput::Read(input)
678    }
679}
680
681impl From<WriteInput> for ToolInput {
682    fn from(input: WriteInput) -> Self {
683        ToolInput::Write(input)
684    }
685}
686
687impl From<EditInput> for ToolInput {
688    fn from(input: EditInput) -> Self {
689        ToolInput::Edit(input)
690    }
691}
692
693impl From<GlobInput> for ToolInput {
694    fn from(input: GlobInput) -> Self {
695        ToolInput::Glob(input)
696    }
697}
698
699impl From<GrepInput> for ToolInput {
700    fn from(input: GrepInput) -> Self {
701        ToolInput::Grep(input)
702    }
703}
704
705impl From<TaskInput> for ToolInput {
706    fn from(input: TaskInput) -> Self {
707        ToolInput::Task(input)
708    }
709}
710
711impl From<WebFetchInput> for ToolInput {
712    fn from(input: WebFetchInput) -> Self {
713        ToolInput::WebFetch(input)
714    }
715}
716
717impl From<WebSearchInput> for ToolInput {
718    fn from(input: WebSearchInput) -> Self {
719        ToolInput::WebSearch(input)
720    }
721}
722
723impl From<TodoWriteInput> for ToolInput {
724    fn from(input: TodoWriteInput) -> Self {
725        ToolInput::TodoWrite(input)
726    }
727}
728
729impl From<AskUserQuestionInput> for ToolInput {
730    fn from(input: AskUserQuestionInput) -> Self {
731        ToolInput::AskUserQuestion(input)
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn test_bash_input_parsing() {
741        let json = serde_json::json!({
742            "command": "ls -la",
743            "description": "List files",
744            "timeout": 5000,
745            "run_in_background": false
746        });
747
748        let input: BashInput = serde_json::from_value(json).unwrap();
749        assert_eq!(input.command, "ls -la");
750        assert_eq!(input.description, Some("List files".to_string()));
751        assert_eq!(input.timeout, Some(5000));
752        assert_eq!(input.run_in_background, Some(false));
753    }
754
755    #[test]
756    fn test_bash_input_minimal() {
757        let json = serde_json::json!({
758            "command": "echo hello"
759        });
760
761        let input: BashInput = serde_json::from_value(json).unwrap();
762        assert_eq!(input.command, "echo hello");
763        assert_eq!(input.description, None);
764        assert_eq!(input.timeout, None);
765    }
766
767    #[test]
768    fn test_read_input_parsing() {
769        let json = serde_json::json!({
770            "file_path": "/home/user/test.rs",
771            "offset": 10,
772            "limit": 100
773        });
774
775        let input: ReadInput = serde_json::from_value(json).unwrap();
776        assert_eq!(input.file_path, "/home/user/test.rs");
777        assert_eq!(input.offset, Some(10));
778        assert_eq!(input.limit, Some(100));
779    }
780
781    #[test]
782    fn test_write_input_parsing() {
783        let json = serde_json::json!({
784            "file_path": "/tmp/test.txt",
785            "content": "Hello, world!"
786        });
787
788        let input: WriteInput = serde_json::from_value(json).unwrap();
789        assert_eq!(input.file_path, "/tmp/test.txt");
790        assert_eq!(input.content, "Hello, world!");
791    }
792
793    #[test]
794    fn test_edit_input_parsing() {
795        let json = serde_json::json!({
796            "file_path": "/home/user/code.rs",
797            "old_string": "fn old()",
798            "new_string": "fn new()",
799            "replace_all": true
800        });
801
802        let input: EditInput = serde_json::from_value(json).unwrap();
803        assert_eq!(input.file_path, "/home/user/code.rs");
804        assert_eq!(input.old_string, "fn old()");
805        assert_eq!(input.new_string, "fn new()");
806        assert_eq!(input.replace_all, Some(true));
807    }
808
809    #[test]
810    fn test_glob_input_parsing() {
811        let json = serde_json::json!({
812            "pattern": "**/*.rs",
813            "path": "/home/user/project"
814        });
815
816        let input: GlobInput = serde_json::from_value(json).unwrap();
817        assert_eq!(input.pattern, "**/*.rs");
818        assert_eq!(input.path, Some("/home/user/project".to_string()));
819    }
820
821    #[test]
822    fn test_grep_input_parsing() {
823        let json = serde_json::json!({
824            "pattern": "fn\\s+\\w+",
825            "path": "/home/user/project",
826            "type": "rust",
827            "-i": true,
828            "-C": 3
829        });
830
831        let input: GrepInput = serde_json::from_value(json).unwrap();
832        assert_eq!(input.pattern, "fn\\s+\\w+");
833        assert_eq!(input.file_type, Some("rust".to_string()));
834        assert_eq!(input.case_insensitive, Some(true));
835        assert_eq!(input.context, Some(3));
836    }
837
838    #[test]
839    fn test_task_input_parsing() {
840        let json = serde_json::json!({
841            "description": "Search codebase",
842            "prompt": "Find all usages of foo()",
843            "subagent_type": "Explore",
844            "run_in_background": true
845        });
846
847        let input: TaskInput = serde_json::from_value(json).unwrap();
848        assert_eq!(input.description, "Search codebase");
849        assert_eq!(input.prompt, "Find all usages of foo()");
850        assert_eq!(input.subagent_type, "Explore");
851        assert_eq!(input.run_in_background, Some(true));
852    }
853
854    #[test]
855    fn test_web_fetch_input_parsing() {
856        let json = serde_json::json!({
857            "url": "https://example.com",
858            "prompt": "Extract the main content"
859        });
860
861        let input: WebFetchInput = serde_json::from_value(json).unwrap();
862        assert_eq!(input.url, "https://example.com");
863        assert_eq!(input.prompt, "Extract the main content");
864    }
865
866    #[test]
867    fn test_web_search_input_parsing() {
868        let json = serde_json::json!({
869            "query": "rust serde tutorial",
870            "allowed_domains": ["docs.rs", "crates.io"]
871        });
872
873        let input: WebSearchInput = serde_json::from_value(json).unwrap();
874        assert_eq!(input.query, "rust serde tutorial");
875        assert_eq!(
876            input.allowed_domains,
877            Some(vec!["docs.rs".to_string(), "crates.io".to_string()])
878        );
879    }
880
881    #[test]
882    fn test_todo_write_input_parsing() {
883        let json = serde_json::json!({
884            "todos": [
885                {
886                    "content": "Fix the bug",
887                    "status": "in_progress",
888                    "activeForm": "Fixing the bug"
889                },
890                {
891                    "content": "Write tests",
892                    "status": "pending",
893                    "activeForm": "Writing tests"
894                }
895            ]
896        });
897
898        let input: TodoWriteInput = serde_json::from_value(json).unwrap();
899        assert_eq!(input.todos.len(), 2);
900        assert_eq!(input.todos[0].content, "Fix the bug");
901        assert_eq!(input.todos[0].status, "in_progress");
902        assert_eq!(input.todos[1].status, "pending");
903    }
904
905    #[test]
906    fn test_ask_user_question_input_parsing() {
907        let json = serde_json::json!({
908            "questions": [
909                {
910                    "question": "Which framework?",
911                    "header": "Framework",
912                    "options": [
913                        {"label": "React", "description": "Popular UI library"},
914                        {"label": "Vue", "description": "Progressive framework"}
915                    ],
916                    "multiSelect": false
917                }
918            ]
919        });
920
921        let input: AskUserQuestionInput = serde_json::from_value(json).unwrap();
922        assert_eq!(input.questions.len(), 1);
923        assert_eq!(input.questions[0].question, "Which framework?");
924        assert_eq!(input.questions[0].options.len(), 2);
925        assert_eq!(input.questions[0].options[0].label, "React");
926    }
927
928    #[test]
929    fn test_tool_input_enum_bash() {
930        let json = serde_json::json!({
931            "command": "ls -la"
932        });
933
934        let input: ToolInput = serde_json::from_value(json).unwrap();
935        assert!(matches!(input, ToolInput::Bash(_)));
936        assert_eq!(input.tool_name(), Some("Bash"));
937        assert!(input.as_bash().is_some());
938    }
939
940    #[test]
941    fn test_tool_input_enum_edit() {
942        let json = serde_json::json!({
943            "file_path": "/test.rs",
944            "old_string": "old",
945            "new_string": "new"
946        });
947
948        let input: ToolInput = serde_json::from_value(json).unwrap();
949        assert!(matches!(input, ToolInput::Edit(_)));
950        assert_eq!(input.tool_name(), Some("Edit"));
951    }
952
953    #[test]
954    fn test_tool_input_enum_unknown() {
955        // Custom MCP tool with unknown structure
956        let json = serde_json::json!({
957            "custom_field": "custom_value",
958            "another_field": 42
959        });
960
961        let input: ToolInput = serde_json::from_value(json).unwrap();
962        assert!(matches!(input, ToolInput::Unknown(_)));
963        assert_eq!(input.tool_name(), None);
964        assert!(input.is_unknown());
965
966        let unknown = input.as_unknown().unwrap();
967        assert_eq!(unknown.get("custom_field").unwrap(), "custom_value");
968    }
969
970    #[test]
971    fn test_tool_input_roundtrip() {
972        let original = BashInput {
973            command: "echo test".to_string(),
974            description: Some("Test command".to_string()),
975            timeout: Some(5000),
976            run_in_background: None,
977        };
978
979        let tool_input: ToolInput = original.clone().into();
980        let json = serde_json::to_value(&tool_input).unwrap();
981        let parsed: ToolInput = serde_json::from_value(json).unwrap();
982
983        if let ToolInput::Bash(bash) = parsed {
984            assert_eq!(bash.command, original.command);
985            assert_eq!(bash.description, original.description);
986        } else {
987            panic!("Expected Bash variant");
988        }
989    }
990
991    #[test]
992    fn test_notebook_edit_input_parsing() {
993        let json = serde_json::json!({
994            "notebook_path": "/home/user/notebook.ipynb",
995            "new_source": "print('hello')",
996            "cell_id": "abc123",
997            "cell_type": "code",
998            "edit_mode": "replace"
999        });
1000
1001        let input: NotebookEditInput = serde_json::from_value(json).unwrap();
1002        assert_eq!(input.notebook_path, "/home/user/notebook.ipynb");
1003        assert_eq!(input.new_source, "print('hello')");
1004        assert_eq!(input.cell_id, Some("abc123".to_string()));
1005    }
1006
1007    #[test]
1008    fn test_task_output_input_parsing() {
1009        let json = serde_json::json!({
1010            "task_id": "task-123",
1011            "block": false,
1012            "timeout": 60000
1013        });
1014
1015        let input: TaskOutputInput = serde_json::from_value(json).unwrap();
1016        assert_eq!(input.task_id, "task-123");
1017        assert!(!input.block);
1018        assert_eq!(input.timeout, 60000);
1019    }
1020
1021    #[test]
1022    fn test_skill_input_parsing() {
1023        let json = serde_json::json!({
1024            "skill": "commit",
1025            "args": "-m 'Fix bug'"
1026        });
1027
1028        let input: SkillInput = serde_json::from_value(json).unwrap();
1029        assert_eq!(input.skill, "commit");
1030        assert_eq!(input.args, Some("-m 'Fix bug'".to_string()));
1031    }
1032}