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 #[serde(rename = "remoteSessionTitle", skip_serializing_if = "Option::is_none")]
390 pub remote_session_title: Option<String>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub plan: Option<String>,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
399pub struct AllowedPrompt {
400 pub tool: String,
402
403 pub prompt: String,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
440#[serde(untagged)]
441pub enum ToolInput {
442 Edit(EditInput),
444
445 Write(WriteInput),
447
448 AskUserQuestion(AskUserQuestionInput),
450
451 TodoWrite(TodoWriteInput),
453
454 Task(TaskInput),
456
457 NotebookEdit(NotebookEditInput),
459
460 WebFetch(WebFetchInput),
462
463 TaskOutput(TaskOutputInput),
465
466 Bash(BashInput),
468
469 Read(ReadInput),
471
472 Glob(GlobInput),
474
475 Grep(GrepInput),
477
478 WebSearch(WebSearchInput),
480
481 KillShell(KillShellInput),
483
484 Skill(SkillInput),
486
487 ExitPlanMode(ExitPlanModeInput),
489
490 EnterPlanMode(EnterPlanModeInput),
492
493 Unknown(Value),
501}
502
503impl ToolInput {
504 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 pub fn as_bash(&self) -> Option<&BashInput> {
533 match self {
534 ToolInput::Bash(input) => Some(input),
535 _ => None,
536 }
537 }
538
539 pub fn as_read(&self) -> Option<&ReadInput> {
541 match self {
542 ToolInput::Read(input) => Some(input),
543 _ => None,
544 }
545 }
546
547 pub fn as_write(&self) -> Option<&WriteInput> {
549 match self {
550 ToolInput::Write(input) => Some(input),
551 _ => None,
552 }
553 }
554
555 pub fn as_edit(&self) -> Option<&EditInput> {
557 match self {
558 ToolInput::Edit(input) => Some(input),
559 _ => None,
560 }
561 }
562
563 pub fn as_glob(&self) -> Option<&GlobInput> {
565 match self {
566 ToolInput::Glob(input) => Some(input),
567 _ => None,
568 }
569 }
570
571 pub fn as_grep(&self) -> Option<&GrepInput> {
573 match self {
574 ToolInput::Grep(input) => Some(input),
575 _ => None,
576 }
577 }
578
579 pub fn as_task(&self) -> Option<&TaskInput> {
581 match self {
582 ToolInput::Task(input) => Some(input),
583 _ => None,
584 }
585 }
586
587 pub fn as_web_fetch(&self) -> Option<&WebFetchInput> {
589 match self {
590 ToolInput::WebFetch(input) => Some(input),
591 _ => None,
592 }
593 }
594
595 pub fn as_web_search(&self) -> Option<&WebSearchInput> {
597 match self {
598 ToolInput::WebSearch(input) => Some(input),
599 _ => None,
600 }
601 }
602
603 pub fn as_todo_write(&self) -> Option<&TodoWriteInput> {
605 match self {
606 ToolInput::TodoWrite(input) => Some(input),
607 _ => None,
608 }
609 }
610
611 pub fn as_ask_user_question(&self) -> Option<&AskUserQuestionInput> {
613 match self {
614 ToolInput::AskUserQuestion(input) => Some(input),
615 _ => None,
616 }
617 }
618
619 pub fn as_notebook_edit(&self) -> Option<&NotebookEditInput> {
621 match self {
622 ToolInput::NotebookEdit(input) => Some(input),
623 _ => None,
624 }
625 }
626
627 pub fn as_task_output(&self) -> Option<&TaskOutputInput> {
629 match self {
630 ToolInput::TaskOutput(input) => Some(input),
631 _ => None,
632 }
633 }
634
635 pub fn as_kill_shell(&self) -> Option<&KillShellInput> {
637 match self {
638 ToolInput::KillShell(input) => Some(input),
639 _ => None,
640 }
641 }
642
643 pub fn as_skill(&self) -> Option<&SkillInput> {
645 match self {
646 ToolInput::Skill(input) => Some(input),
647 _ => None,
648 }
649 }
650
651 pub fn as_unknown(&self) -> Option<&Value> {
653 match self {
654 ToolInput::Unknown(value) => Some(value),
655 _ => None,
656 }
657 }
658
659 pub fn is_unknown(&self) -> bool {
661 matches!(self, ToolInput::Unknown(_))
662 }
663}
664
665impl 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 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}