1use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::collections::HashMap;
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct BashInput {
38 pub command: String,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub description: Option<String>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub timeout: Option<u64>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub run_in_background: Option<bool>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub struct ReadInput {
57 pub file_path: String,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub offset: Option<i64>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub limit: Option<i64>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct WriteInput {
72 pub file_path: String,
74
75 pub content: String,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct EditInput {
82 pub file_path: String,
84
85 pub old_string: String,
87
88 pub new_string: String,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub replace_all: Option<bool>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102#[serde(deny_unknown_fields)]
103pub struct GlobInput {
104 pub pattern: String,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub path: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
114pub struct GrepInput {
115 pub pattern: String,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub path: Option<String>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub glob: Option<String>,
125
126 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
128 pub file_type: Option<String>,
129
130 #[serde(rename = "-i", skip_serializing_if = "Option::is_none")]
132 pub case_insensitive: Option<bool>,
133
134 #[serde(rename = "-n", skip_serializing_if = "Option::is_none")]
136 pub line_numbers: Option<bool>,
137
138 #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
140 pub after_context: Option<u32>,
141
142 #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
144 pub before_context: Option<u32>,
145
146 #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
148 pub context: Option<u32>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub output_mode: Option<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub multiline: Option<bool>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub head_limit: Option<u32>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub offset: Option<u32>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub struct TaskInput {
170 pub description: String,
172
173 pub prompt: String,
175
176 pub subagent_type: String,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub run_in_background: Option<bool>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub model: Option<String>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub max_turns: Option<u32>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub resume: Option<String>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198pub struct WebFetchInput {
199 pub url: String,
201
202 pub prompt: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
208pub struct WebSearchInput {
209 pub query: String,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub allowed_domains: Option<Vec<String>>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub blocked_domains: Option<Vec<String>>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
223pub struct TodoWriteInput {
224 pub todos: Vec<TodoItem>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub struct TodoItem {
231 pub content: String,
233
234 pub status: String,
236
237 #[serde(rename = "activeForm")]
239 pub active_form: String,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244pub struct AskUserQuestionInput {
245 pub questions: Vec<Question>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub answers: Option<HashMap<String, String>>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub metadata: Option<QuestionMetadata>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259pub struct Question {
260 pub question: String,
262
263 pub header: String,
265
266 pub options: Vec<QuestionOption>,
268
269 #[serde(rename = "multiSelect", default)]
271 pub multi_select: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276pub struct QuestionOption {
277 pub label: String,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub description: Option<String>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct QuestionMetadata {
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub source: Option<String>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
295pub struct NotebookEditInput {
296 pub notebook_path: String,
298
299 pub new_source: String,
301
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub cell_id: Option<String>,
305
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub cell_type: Option<String>,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub edit_mode: Option<String>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct TaskOutputInput {
318 pub task_id: String,
320
321 #[serde(default = "default_true")]
323 pub block: bool,
324
325 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340pub struct KillShellInput {
341 pub shell_id: String,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
347pub struct SkillInput {
348 pub skill: String,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub args: Option<String>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
362#[serde(deny_unknown_fields)]
363pub struct EnterPlanModeInput {}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
370#[serde(deny_unknown_fields)]
371pub struct ExitPlanModeInput {
372 #[serde(rename = "allowedPrompts", skip_serializing_if = "Option::is_none")]
374 pub allowed_prompts: Option<Vec<AllowedPrompt>>,
375
376 #[serde(rename = "pushToRemote", skip_serializing_if = "Option::is_none")]
378 pub push_to_remote: Option<bool>,
379
380 #[serde(rename = "remoteSessionId", skip_serializing_if = "Option::is_none")]
382 pub remote_session_id: Option<String>,
383
384 #[serde(rename = "remoteSessionUrl", skip_serializing_if = "Option::is_none")]
386 pub remote_session_url: Option<String>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
391pub struct AllowedPrompt {
392 pub tool: String,
394
395 pub prompt: String,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
432#[serde(untagged)]
433pub enum ToolInput {
434 Edit(EditInput),
436
437 Write(WriteInput),
439
440 AskUserQuestion(AskUserQuestionInput),
442
443 TodoWrite(TodoWriteInput),
445
446 Task(TaskInput),
448
449 NotebookEdit(NotebookEditInput),
451
452 WebFetch(WebFetchInput),
454
455 TaskOutput(TaskOutputInput),
457
458 Bash(BashInput),
460
461 Read(ReadInput),
463
464 Glob(GlobInput),
466
467 Grep(GrepInput),
469
470 WebSearch(WebSearchInput),
472
473 KillShell(KillShellInput),
475
476 Skill(SkillInput),
478
479 ExitPlanMode(ExitPlanModeInput),
481
482 EnterPlanMode(EnterPlanModeInput),
484
485 Unknown(Value),
493}
494
495impl ToolInput {
496 pub fn tool_name(&self) -> Option<&'static str> {
501 match self {
502 ToolInput::Bash(_) => Some("Bash"),
503 ToolInput::Read(_) => Some("Read"),
504 ToolInput::Write(_) => Some("Write"),
505 ToolInput::Edit(_) => Some("Edit"),
506 ToolInput::Glob(_) => Some("Glob"),
507 ToolInput::Grep(_) => Some("Grep"),
508 ToolInput::Task(_) => Some("Task"),
509 ToolInput::WebFetch(_) => Some("WebFetch"),
510 ToolInput::WebSearch(_) => Some("WebSearch"),
511 ToolInput::TodoWrite(_) => Some("TodoWrite"),
512 ToolInput::AskUserQuestion(_) => Some("AskUserQuestion"),
513 ToolInput::NotebookEdit(_) => Some("NotebookEdit"),
514 ToolInput::TaskOutput(_) => Some("TaskOutput"),
515 ToolInput::KillShell(_) => Some("KillShell"),
516 ToolInput::Skill(_) => Some("Skill"),
517 ToolInput::EnterPlanMode(_) => Some("EnterPlanMode"),
518 ToolInput::ExitPlanMode(_) => Some("ExitPlanMode"),
519 ToolInput::Unknown(_) => None,
520 }
521 }
522
523 pub fn as_bash(&self) -> Option<&BashInput> {
525 match self {
526 ToolInput::Bash(input) => Some(input),
527 _ => None,
528 }
529 }
530
531 pub fn as_read(&self) -> Option<&ReadInput> {
533 match self {
534 ToolInput::Read(input) => Some(input),
535 _ => None,
536 }
537 }
538
539 pub fn as_write(&self) -> Option<&WriteInput> {
541 match self {
542 ToolInput::Write(input) => Some(input),
543 _ => None,
544 }
545 }
546
547 pub fn as_edit(&self) -> Option<&EditInput> {
549 match self {
550 ToolInput::Edit(input) => Some(input),
551 _ => None,
552 }
553 }
554
555 pub fn as_glob(&self) -> Option<&GlobInput> {
557 match self {
558 ToolInput::Glob(input) => Some(input),
559 _ => None,
560 }
561 }
562
563 pub fn as_grep(&self) -> Option<&GrepInput> {
565 match self {
566 ToolInput::Grep(input) => Some(input),
567 _ => None,
568 }
569 }
570
571 pub fn as_task(&self) -> Option<&TaskInput> {
573 match self {
574 ToolInput::Task(input) => Some(input),
575 _ => None,
576 }
577 }
578
579 pub fn as_web_fetch(&self) -> Option<&WebFetchInput> {
581 match self {
582 ToolInput::WebFetch(input) => Some(input),
583 _ => None,
584 }
585 }
586
587 pub fn as_web_search(&self) -> Option<&WebSearchInput> {
589 match self {
590 ToolInput::WebSearch(input) => Some(input),
591 _ => None,
592 }
593 }
594
595 pub fn as_todo_write(&self) -> Option<&TodoWriteInput> {
597 match self {
598 ToolInput::TodoWrite(input) => Some(input),
599 _ => None,
600 }
601 }
602
603 pub fn as_ask_user_question(&self) -> Option<&AskUserQuestionInput> {
605 match self {
606 ToolInput::AskUserQuestion(input) => Some(input),
607 _ => None,
608 }
609 }
610
611 pub fn as_notebook_edit(&self) -> Option<&NotebookEditInput> {
613 match self {
614 ToolInput::NotebookEdit(input) => Some(input),
615 _ => None,
616 }
617 }
618
619 pub fn as_task_output(&self) -> Option<&TaskOutputInput> {
621 match self {
622 ToolInput::TaskOutput(input) => Some(input),
623 _ => None,
624 }
625 }
626
627 pub fn as_kill_shell(&self) -> Option<&KillShellInput> {
629 match self {
630 ToolInput::KillShell(input) => Some(input),
631 _ => None,
632 }
633 }
634
635 pub fn as_skill(&self) -> Option<&SkillInput> {
637 match self {
638 ToolInput::Skill(input) => Some(input),
639 _ => None,
640 }
641 }
642
643 pub fn as_unknown(&self) -> Option<&Value> {
645 match self {
646 ToolInput::Unknown(value) => Some(value),
647 _ => None,
648 }
649 }
650
651 pub fn is_unknown(&self) -> bool {
653 matches!(self, ToolInput::Unknown(_))
654 }
655}
656
657impl From<BashInput> for ToolInput {
662 fn from(input: BashInput) -> Self {
663 ToolInput::Bash(input)
664 }
665}
666
667impl From<ReadInput> for ToolInput {
668 fn from(input: ReadInput) -> Self {
669 ToolInput::Read(input)
670 }
671}
672
673impl From<WriteInput> for ToolInput {
674 fn from(input: WriteInput) -> Self {
675 ToolInput::Write(input)
676 }
677}
678
679impl From<EditInput> for ToolInput {
680 fn from(input: EditInput) -> Self {
681 ToolInput::Edit(input)
682 }
683}
684
685impl From<GlobInput> for ToolInput {
686 fn from(input: GlobInput) -> Self {
687 ToolInput::Glob(input)
688 }
689}
690
691impl From<GrepInput> for ToolInput {
692 fn from(input: GrepInput) -> Self {
693 ToolInput::Grep(input)
694 }
695}
696
697impl From<TaskInput> for ToolInput {
698 fn from(input: TaskInput) -> Self {
699 ToolInput::Task(input)
700 }
701}
702
703impl From<WebFetchInput> for ToolInput {
704 fn from(input: WebFetchInput) -> Self {
705 ToolInput::WebFetch(input)
706 }
707}
708
709impl From<WebSearchInput> for ToolInput {
710 fn from(input: WebSearchInput) -> Self {
711 ToolInput::WebSearch(input)
712 }
713}
714
715impl From<TodoWriteInput> for ToolInput {
716 fn from(input: TodoWriteInput) -> Self {
717 ToolInput::TodoWrite(input)
718 }
719}
720
721impl From<AskUserQuestionInput> for ToolInput {
722 fn from(input: AskUserQuestionInput) -> Self {
723 ToolInput::AskUserQuestion(input)
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 #[test]
732 fn test_bash_input_parsing() {
733 let json = serde_json::json!({
734 "command": "ls -la",
735 "description": "List files",
736 "timeout": 5000,
737 "run_in_background": false
738 });
739
740 let input: BashInput = serde_json::from_value(json).unwrap();
741 assert_eq!(input.command, "ls -la");
742 assert_eq!(input.description, Some("List files".to_string()));
743 assert_eq!(input.timeout, Some(5000));
744 assert_eq!(input.run_in_background, Some(false));
745 }
746
747 #[test]
748 fn test_bash_input_minimal() {
749 let json = serde_json::json!({
750 "command": "echo hello"
751 });
752
753 let input: BashInput = serde_json::from_value(json).unwrap();
754 assert_eq!(input.command, "echo hello");
755 assert_eq!(input.description, None);
756 assert_eq!(input.timeout, None);
757 }
758
759 #[test]
760 fn test_read_input_parsing() {
761 let json = serde_json::json!({
762 "file_path": "/home/user/test.rs",
763 "offset": 10,
764 "limit": 100
765 });
766
767 let input: ReadInput = serde_json::from_value(json).unwrap();
768 assert_eq!(input.file_path, "/home/user/test.rs");
769 assert_eq!(input.offset, Some(10));
770 assert_eq!(input.limit, Some(100));
771 }
772
773 #[test]
774 fn test_write_input_parsing() {
775 let json = serde_json::json!({
776 "file_path": "/tmp/test.txt",
777 "content": "Hello, world!"
778 });
779
780 let input: WriteInput = serde_json::from_value(json).unwrap();
781 assert_eq!(input.file_path, "/tmp/test.txt");
782 assert_eq!(input.content, "Hello, world!");
783 }
784
785 #[test]
786 fn test_edit_input_parsing() {
787 let json = serde_json::json!({
788 "file_path": "/home/user/code.rs",
789 "old_string": "fn old()",
790 "new_string": "fn new()",
791 "replace_all": true
792 });
793
794 let input: EditInput = serde_json::from_value(json).unwrap();
795 assert_eq!(input.file_path, "/home/user/code.rs");
796 assert_eq!(input.old_string, "fn old()");
797 assert_eq!(input.new_string, "fn new()");
798 assert_eq!(input.replace_all, Some(true));
799 }
800
801 #[test]
802 fn test_glob_input_parsing() {
803 let json = serde_json::json!({
804 "pattern": "**/*.rs",
805 "path": "/home/user/project"
806 });
807
808 let input: GlobInput = serde_json::from_value(json).unwrap();
809 assert_eq!(input.pattern, "**/*.rs");
810 assert_eq!(input.path, Some("/home/user/project".to_string()));
811 }
812
813 #[test]
814 fn test_grep_input_parsing() {
815 let json = serde_json::json!({
816 "pattern": "fn\\s+\\w+",
817 "path": "/home/user/project",
818 "type": "rust",
819 "-i": true,
820 "-C": 3
821 });
822
823 let input: GrepInput = serde_json::from_value(json).unwrap();
824 assert_eq!(input.pattern, "fn\\s+\\w+");
825 assert_eq!(input.file_type, Some("rust".to_string()));
826 assert_eq!(input.case_insensitive, Some(true));
827 assert_eq!(input.context, Some(3));
828 }
829
830 #[test]
831 fn test_task_input_parsing() {
832 let json = serde_json::json!({
833 "description": "Search codebase",
834 "prompt": "Find all usages of foo()",
835 "subagent_type": "Explore",
836 "run_in_background": true
837 });
838
839 let input: TaskInput = serde_json::from_value(json).unwrap();
840 assert_eq!(input.description, "Search codebase");
841 assert_eq!(input.prompt, "Find all usages of foo()");
842 assert_eq!(input.subagent_type, "Explore");
843 assert_eq!(input.run_in_background, Some(true));
844 }
845
846 #[test]
847 fn test_web_fetch_input_parsing() {
848 let json = serde_json::json!({
849 "url": "https://example.com",
850 "prompt": "Extract the main content"
851 });
852
853 let input: WebFetchInput = serde_json::from_value(json).unwrap();
854 assert_eq!(input.url, "https://example.com");
855 assert_eq!(input.prompt, "Extract the main content");
856 }
857
858 #[test]
859 fn test_web_search_input_parsing() {
860 let json = serde_json::json!({
861 "query": "rust serde tutorial",
862 "allowed_domains": ["docs.rs", "crates.io"]
863 });
864
865 let input: WebSearchInput = serde_json::from_value(json).unwrap();
866 assert_eq!(input.query, "rust serde tutorial");
867 assert_eq!(
868 input.allowed_domains,
869 Some(vec!["docs.rs".to_string(), "crates.io".to_string()])
870 );
871 }
872
873 #[test]
874 fn test_todo_write_input_parsing() {
875 let json = serde_json::json!({
876 "todos": [
877 {
878 "content": "Fix the bug",
879 "status": "in_progress",
880 "activeForm": "Fixing the bug"
881 },
882 {
883 "content": "Write tests",
884 "status": "pending",
885 "activeForm": "Writing tests"
886 }
887 ]
888 });
889
890 let input: TodoWriteInput = serde_json::from_value(json).unwrap();
891 assert_eq!(input.todos.len(), 2);
892 assert_eq!(input.todos[0].content, "Fix the bug");
893 assert_eq!(input.todos[0].status, "in_progress");
894 assert_eq!(input.todos[1].status, "pending");
895 }
896
897 #[test]
898 fn test_ask_user_question_input_parsing() {
899 let json = serde_json::json!({
900 "questions": [
901 {
902 "question": "Which framework?",
903 "header": "Framework",
904 "options": [
905 {"label": "React", "description": "Popular UI library"},
906 {"label": "Vue", "description": "Progressive framework"}
907 ],
908 "multiSelect": false
909 }
910 ]
911 });
912
913 let input: AskUserQuestionInput = serde_json::from_value(json).unwrap();
914 assert_eq!(input.questions.len(), 1);
915 assert_eq!(input.questions[0].question, "Which framework?");
916 assert_eq!(input.questions[0].options.len(), 2);
917 assert_eq!(input.questions[0].options[0].label, "React");
918 }
919
920 #[test]
921 fn test_tool_input_enum_bash() {
922 let json = serde_json::json!({
923 "command": "ls -la"
924 });
925
926 let input: ToolInput = serde_json::from_value(json).unwrap();
927 assert!(matches!(input, ToolInput::Bash(_)));
928 assert_eq!(input.tool_name(), Some("Bash"));
929 assert!(input.as_bash().is_some());
930 }
931
932 #[test]
933 fn test_tool_input_enum_edit() {
934 let json = serde_json::json!({
935 "file_path": "/test.rs",
936 "old_string": "old",
937 "new_string": "new"
938 });
939
940 let input: ToolInput = serde_json::from_value(json).unwrap();
941 assert!(matches!(input, ToolInput::Edit(_)));
942 assert_eq!(input.tool_name(), Some("Edit"));
943 }
944
945 #[test]
946 fn test_tool_input_enum_unknown() {
947 let json = serde_json::json!({
949 "custom_field": "custom_value",
950 "another_field": 42
951 });
952
953 let input: ToolInput = serde_json::from_value(json).unwrap();
954 assert!(matches!(input, ToolInput::Unknown(_)));
955 assert_eq!(input.tool_name(), None);
956 assert!(input.is_unknown());
957
958 let unknown = input.as_unknown().unwrap();
959 assert_eq!(unknown.get("custom_field").unwrap(), "custom_value");
960 }
961
962 #[test]
963 fn test_tool_input_roundtrip() {
964 let original = BashInput {
965 command: "echo test".to_string(),
966 description: Some("Test command".to_string()),
967 timeout: Some(5000),
968 run_in_background: None,
969 };
970
971 let tool_input: ToolInput = original.clone().into();
972 let json = serde_json::to_value(&tool_input).unwrap();
973 let parsed: ToolInput = serde_json::from_value(json).unwrap();
974
975 if let ToolInput::Bash(bash) = parsed {
976 assert_eq!(bash.command, original.command);
977 assert_eq!(bash.description, original.description);
978 } else {
979 panic!("Expected Bash variant");
980 }
981 }
982
983 #[test]
984 fn test_notebook_edit_input_parsing() {
985 let json = serde_json::json!({
986 "notebook_path": "/home/user/notebook.ipynb",
987 "new_source": "print('hello')",
988 "cell_id": "abc123",
989 "cell_type": "code",
990 "edit_mode": "replace"
991 });
992
993 let input: NotebookEditInput = serde_json::from_value(json).unwrap();
994 assert_eq!(input.notebook_path, "/home/user/notebook.ipynb");
995 assert_eq!(input.new_source, "print('hello')");
996 assert_eq!(input.cell_id, Some("abc123".to_string()));
997 }
998
999 #[test]
1000 fn test_task_output_input_parsing() {
1001 let json = serde_json::json!({
1002 "task_id": "task-123",
1003 "block": false,
1004 "timeout": 60000
1005 });
1006
1007 let input: TaskOutputInput = serde_json::from_value(json).unwrap();
1008 assert_eq!(input.task_id, "task-123");
1009 assert!(!input.block);
1010 assert_eq!(input.timeout, 60000);
1011 }
1012
1013 #[test]
1014 fn test_skill_input_parsing() {
1015 let json = serde_json::json!({
1016 "skill": "commit",
1017 "args": "-m 'Fix bug'"
1018 });
1019
1020 let input: SkillInput = serde_json::from_value(json).unwrap();
1021 assert_eq!(input.skill, "commit");
1022 assert_eq!(input.args, Some("-m 'Fix bug'".to_string()));
1023 }
1024}