Skip to main content

ralph/cli/task/
mod.rs

1//! `ralph task ...` command group: Clap types and handler facade.
2//!
3//! Responsibilities:
4//! - Define clap structures for task-related commands (re-exported from submodules).
5//! - Route task subcommands to their specific handlers.
6//! - Re-export argument types used by task commands.
7//!
8//! Not handled here:
9//! - Queue persistence and locking semantics (see `crate::queue` and `crate::lock`).
10//! - Task execution or runner behavior.
11//!
12//! Invariants/assumptions:
13//! - Configuration is resolved from the current working directory.
14//! - Task state changes occur within the subcommand handlers.
15
16mod args;
17mod batch;
18mod build;
19mod children;
20mod clone;
21mod edit;
22mod from_template;
23mod parent;
24mod refactor;
25mod relations;
26mod schedule;
27mod show;
28mod split;
29mod start;
30mod status;
31mod template;
32
33use anyhow::Result;
34
35use crate::config;
36
37// Re-export all argument types for backward compatibility
38pub use args::{
39    BatchEditArgs, BatchFieldArgs, BatchMode, BatchOperation, BatchStatusArgs, TaskArgs,
40    TaskBatchArgs, TaskBlocksArgs, TaskBuildArgs, TaskBuildRefactorArgs, TaskChildrenArgs,
41    TaskCloneArgs, TaskCommand, TaskDoneArgs, TaskEditArgs, TaskEditFieldArg, TaskFieldArgs,
42    TaskFromArgs, TaskFromCommand, TaskFromTemplateArgs, TaskMarkDuplicateArgs, TaskParentArgs,
43    TaskReadyArgs, TaskRejectArgs, TaskRelateArgs, TaskRelationFormat, TaskScheduleArgs,
44    TaskShowArgs, TaskSplitArgs, TaskStartArgs, TaskStatusArg, TaskStatusArgs, TaskTemplateArgs,
45    TaskTemplateBuildArgs, TaskTemplateCommand, TaskTemplateShowArgs, TaskUpdateArgs,
46};
47
48/// Main entry point for task commands.
49pub fn handle_task(args: TaskArgs, force: bool) -> Result<()> {
50    let resolved = config::resolve_from_cwd()?;
51
52    match args.command {
53        Some(TaskCommand::Ready(args)) => status::handle_ready(&args, force, &resolved),
54        Some(TaskCommand::Status(args)) => status::handle_status(&args, force, &resolved),
55        Some(TaskCommand::Done(args)) => status::handle_done(&args, force, &resolved),
56        Some(TaskCommand::Reject(args)) => status::handle_reject(&args, force, &resolved),
57        Some(TaskCommand::Field(args)) => edit::handle_field(&args, force, &resolved),
58        Some(TaskCommand::Edit(args)) => edit::handle_edit(&args, force, &resolved),
59        Some(TaskCommand::Update(args)) => edit::handle_update(&args, &resolved, force),
60        Some(TaskCommand::Build(args)) => build::handle(&args, force, &resolved),
61        Some(TaskCommand::Template(template_args)) => template::handle(&resolved, &template_args),
62        Some(TaskCommand::BuildRefactor(args)) | Some(TaskCommand::Refactor(args)) => {
63            refactor::handle(&args, force, &resolved)
64        }
65        Some(TaskCommand::Show(args)) => show::handle(&args, &resolved),
66        Some(TaskCommand::Clone(args)) => clone::handle(&args, force, &resolved),
67        Some(TaskCommand::Batch(args)) => batch::handle(&args, force, &resolved),
68        Some(TaskCommand::Schedule(args)) => schedule::handle(&args, force, &resolved),
69        Some(TaskCommand::Relate(args)) => relations::handle_relate(&args, force, &resolved),
70        Some(TaskCommand::Blocks(args)) => relations::handle_blocks(&args, force, &resolved),
71        Some(TaskCommand::MarkDuplicate(args)) => {
72            relations::handle_mark_duplicate(&args, force, &resolved)
73        }
74        Some(TaskCommand::Split(args)) => split::handle(&args, force, &resolved),
75        Some(TaskCommand::Start(args)) => start::handle(&args, force, &resolved),
76        Some(TaskCommand::Children(args)) => children::handle(&args, &resolved),
77        Some(TaskCommand::Parent(args)) => parent::handle(&args, &resolved),
78        Some(TaskCommand::From(args)) => match args.command {
79            TaskFromCommand::Template(template_args) => {
80                from_template::handle(&resolved, &template_args, force)
81            }
82        },
83        None => {
84            // Default command: build from request
85            build::handle(&args.build, force, &resolved)
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use clap::{CommandFactory, Parser};
93
94    use crate::cli::Cli;
95    use crate::cli::queue::QueueShowFormat;
96    use crate::cli::task::args::{BatchOperation, TaskEditFieldArg, TaskStatusArg};
97
98    #[test]
99    fn task_update_help_mentions_rp_examples() {
100        let mut cmd = Cli::command();
101        let task = cmd.find_subcommand_mut("task").expect("task subcommand");
102        let update = task
103            .find_subcommand_mut("update")
104            .expect("task update subcommand");
105        let help = update.render_long_help().to_string();
106
107        assert!(
108            help.contains("ralph task update --repo-prompt plan RQ-0001"),
109            "missing repo-prompt plan example: {help}"
110        );
111        assert!(
112            help.contains("ralph task update --repo-prompt off --fields scope,evidence RQ-0001"),
113            "missing repo-prompt off example: {help}"
114        );
115        assert!(
116            help.contains("ralph task update --approval-mode auto-edits --runner claude RQ-0001"),
117            "missing approval-mode example: {help}"
118        );
119    }
120
121    #[test]
122    fn task_show_help_mentions_examples() {
123        let mut cmd = Cli::command();
124        let task = cmd.find_subcommand_mut("task").expect("task subcommand");
125        let show = task
126            .find_subcommand_mut("show")
127            .expect("task show subcommand");
128        let help = show.render_long_help().to_string();
129
130        assert!(
131            help.contains("ralph task show RQ-0001"),
132            "missing show example: {help}"
133        );
134        assert!(
135            help.contains("--format compact"),
136            "missing format example: {help}"
137        );
138    }
139
140    #[test]
141    fn task_details_alias_parses() {
142        let cli =
143            Cli::try_parse_from(["ralph", "task", "details", "RQ-0001", "--format", "compact"])
144                .expect("parse");
145
146        match cli.command {
147            crate::cli::Command::Task(args) => match args.command {
148                Some(crate::cli::task::TaskCommand::Show(args)) => {
149                    assert_eq!(args.task_id, "RQ-0001");
150                    assert_eq!(args.format, QueueShowFormat::Compact);
151                }
152                _ => panic!("expected task show command"),
153            },
154            _ => panic!("expected task command"),
155        }
156    }
157
158    #[test]
159    fn task_build_parses_repo_prompt_and_effort_alias() {
160        let cli = Cli::try_parse_from([
161            "ralph",
162            "task",
163            "build",
164            "--repo-prompt",
165            "plan",
166            "-e",
167            "high",
168            "Add tests",
169        ])
170        .expect("parse");
171
172        match cli.command {
173            crate::cli::Command::Task(args) => match args.command {
174                Some(crate::cli::task::TaskCommand::Build(args)) => {
175                    assert_eq!(args.repo_prompt, Some(crate::agent::RepoPromptMode::Plan));
176                    assert_eq!(args.effort.as_deref(), Some("high"));
177                }
178                _ => panic!("expected task build command"),
179            },
180            _ => panic!("expected task command"),
181        }
182    }
183
184    #[test]
185    fn task_build_parses_runner_cli_overrides() {
186        let cli = Cli::try_parse_from([
187            "ralph",
188            "task",
189            "build",
190            "--approval-mode",
191            "yolo",
192            "--sandbox",
193            "disabled",
194            "Add tests",
195        ])
196        .expect("parse");
197
198        match cli.command {
199            crate::cli::Command::Task(args) => match args.command {
200                Some(crate::cli::task::TaskCommand::Build(args)) => {
201                    assert_eq!(args.runner_cli.approval_mode.as_deref(), Some("yolo"));
202                    assert_eq!(args.runner_cli.sandbox.as_deref(), Some("disabled"));
203                }
204                _ => panic!("expected task build command"),
205            },
206            _ => panic!("expected task command"),
207        }
208    }
209
210    #[test]
211    fn task_update_parses_repo_prompt_and_effort_alias() {
212        let cli = Cli::try_parse_from([
213            "ralph",
214            "task",
215            "update",
216            "--repo-prompt",
217            "off",
218            "-e",
219            "low",
220            "RQ-0001",
221        ])
222        .expect("parse");
223
224        match cli.command {
225            crate::cli::Command::Task(args) => match args.command {
226                Some(crate::cli::task::TaskCommand::Update(args)) => {
227                    assert_eq!(args.repo_prompt, Some(crate::agent::RepoPromptMode::Off));
228                    assert_eq!(args.effort.as_deref(), Some("low"));
229                    assert_eq!(args.task_id.as_deref(), Some("RQ-0001"));
230                }
231                _ => panic!("expected task update command"),
232            },
233            _ => panic!("expected task command"),
234        }
235    }
236
237    #[test]
238    fn task_update_parses_runner_cli_overrides() {
239        let cli = Cli::try_parse_from([
240            "ralph",
241            "task",
242            "update",
243            "--approval-mode",
244            "auto-edits",
245            "--sandbox",
246            "disabled",
247            "RQ-0001",
248        ])
249        .expect("parse");
250
251        match cli.command {
252            crate::cli::Command::Task(args) => match args.command {
253                Some(crate::cli::task::TaskCommand::Update(args)) => {
254                    assert_eq!(args.runner_cli.approval_mode.as_deref(), Some("auto-edits"));
255                    assert_eq!(args.runner_cli.sandbox.as_deref(), Some("disabled"));
256                    assert_eq!(args.task_id.as_deref(), Some("RQ-0001"));
257                }
258                _ => panic!("expected task update command"),
259            },
260            _ => panic!("expected task command"),
261        }
262    }
263
264    #[test]
265    fn task_edit_parses_dry_run_flag() {
266        let cli = Cli::try_parse_from([
267            "ralph",
268            "task",
269            "edit",
270            "--dry-run",
271            "title",
272            "New title",
273            "RQ-0001",
274        ])
275        .expect("parse");
276
277        match cli.command {
278            crate::cli::Command::Task(args) => match args.command {
279                Some(crate::cli::task::TaskCommand::Edit(args)) => {
280                    assert!(args.dry_run);
281                    assert_eq!(args.task_ids, vec!["RQ-0001"]);
282                    assert_eq!(args.value, "New title");
283                }
284                _ => panic!("expected task edit command"),
285            },
286            _ => panic!("expected task command"),
287        }
288    }
289
290    #[test]
291    fn task_edit_without_dry_run_defaults_to_false() {
292        let cli = Cli::try_parse_from(["ralph", "task", "edit", "title", "New title", "RQ-0001"])
293            .expect("parse");
294
295        match cli.command {
296            crate::cli::Command::Task(args) => match args.command {
297                Some(crate::cli::task::TaskCommand::Edit(args)) => {
298                    assert!(!args.dry_run);
299                }
300                _ => panic!("expected task edit command"),
301            },
302            _ => panic!("expected task command"),
303        }
304    }
305
306    #[test]
307    fn task_update_parses_dry_run_flag() {
308        let cli = Cli::try_parse_from(["ralph", "task", "update", "--dry-run", "RQ-0001"])
309            .expect("parse");
310
311        match cli.command {
312            crate::cli::Command::Task(args) => match args.command {
313                Some(crate::cli::task::TaskCommand::Update(args)) => {
314                    assert!(args.dry_run);
315                    assert_eq!(args.task_id.as_deref(), Some("RQ-0001"));
316                }
317                _ => panic!("expected task update command"),
318            },
319            _ => panic!("expected task command"),
320        }
321    }
322
323    #[test]
324    fn task_update_without_dry_run_defaults_to_false() {
325        let cli = Cli::try_parse_from(["ralph", "task", "update", "RQ-0001"]).expect("parse");
326
327        match cli.command {
328            crate::cli::Command::Task(args) => match args.command {
329                Some(crate::cli::task::TaskCommand::Update(args)) => {
330                    assert!(!args.dry_run);
331                }
332                _ => panic!("expected task update command"),
333            },
334            _ => panic!("expected task command"),
335        }
336    }
337
338    #[test]
339    fn task_refactor_parses() {
340        let cli = Cli::try_parse_from(["ralph", "task", "refactor"]).expect("parse");
341        match cli.command {
342            crate::cli::Command::Task(args) => match args.command {
343                Some(crate::cli::task::TaskCommand::Refactor(_)) => {}
344                _ => panic!("expected task refactor command"),
345            },
346            _ => panic!("expected task command"),
347        }
348    }
349
350    #[test]
351    fn task_ref_alias_parses() {
352        let cli =
353            Cli::try_parse_from(["ralph", "task", "ref", "--threshold", "800"]).expect("parse");
354        match cli.command {
355            crate::cli::Command::Task(args) => match args.command {
356                Some(crate::cli::task::TaskCommand::Refactor(args)) => {
357                    assert_eq!(args.threshold, 800);
358                }
359                _ => panic!("expected task refactor command via alias"),
360            },
361            _ => panic!("expected task command"),
362        }
363    }
364
365    #[test]
366    fn task_build_refactor_parses() {
367        let cli = Cli::try_parse_from(["ralph", "task", "build-refactor", "--threshold", "700"])
368            .expect("parse");
369        match cli.command {
370            crate::cli::Command::Task(args) => match args.command {
371                Some(crate::cli::task::TaskCommand::BuildRefactor(args)) => {
372                    assert_eq!(args.threshold, 700);
373                }
374                _ => panic!("expected task build-refactor command"),
375            },
376            _ => panic!("expected task command"),
377        }
378    }
379
380    #[test]
381    fn task_clone_parses() {
382        let cli = Cli::try_parse_from(["ralph", "task", "clone", "RQ-0001"]).expect("parse");
383        match cli.command {
384            crate::cli::Command::Task(args) => match args.command {
385                Some(crate::cli::task::TaskCommand::Clone(args)) => {
386                    assert_eq!(args.task_id, "RQ-0001");
387                    assert!(!args.dry_run);
388                }
389                _ => panic!("expected task clone command"),
390            },
391            _ => panic!("expected task command"),
392        }
393    }
394
395    #[test]
396    fn task_duplicate_alias_parses() {
397        let cli = Cli::try_parse_from(["ralph", "task", "duplicate", "RQ-0001"]).expect("parse");
398        match cli.command {
399            crate::cli::Command::Task(args) => match args.command {
400                Some(crate::cli::task::TaskCommand::Clone(args)) => {
401                    assert_eq!(args.task_id, "RQ-0001");
402                }
403                _ => panic!("expected task clone command via duplicate alias"),
404            },
405            _ => panic!("expected task command"),
406        }
407    }
408
409    #[test]
410    fn task_clone_parses_status_flag() {
411        let cli = Cli::try_parse_from(["ralph", "task", "clone", "--status", "todo", "RQ-0001"])
412            .expect("parse");
413        match cli.command {
414            crate::cli::Command::Task(args) => match args.command {
415                Some(crate::cli::task::TaskCommand::Clone(args)) => {
416                    assert_eq!(args.task_id, "RQ-0001");
417                    assert_eq!(args.status, Some(TaskStatusArg::Todo));
418                }
419                _ => panic!("expected task clone command"),
420            },
421            _ => panic!("expected task command"),
422        }
423    }
424
425    #[test]
426    fn task_clone_parses_title_prefix() {
427        let cli = Cli::try_parse_from([
428            "ralph",
429            "task",
430            "clone",
431            "--title-prefix",
432            "[Follow-up] ",
433            "RQ-0001",
434        ])
435        .expect("parse");
436        match cli.command {
437            crate::cli::Command::Task(args) => match args.command {
438                Some(crate::cli::task::TaskCommand::Clone(args)) => {
439                    assert_eq!(args.task_id, "RQ-0001");
440                    assert_eq!(args.title_prefix, Some("[Follow-up] ".to_string()));
441                }
442                _ => panic!("expected task clone command"),
443            },
444            _ => panic!("expected task command"),
445        }
446    }
447
448    #[test]
449    fn task_clone_parses_dry_run_flag() {
450        let cli =
451            Cli::try_parse_from(["ralph", "task", "clone", "--dry-run", "RQ-0001"]).expect("parse");
452        match cli.command {
453            crate::cli::Command::Task(args) => match args.command {
454                Some(crate::cli::task::TaskCommand::Clone(args)) => {
455                    assert_eq!(args.task_id, "RQ-0001");
456                    assert!(args.dry_run);
457                }
458                _ => panic!("expected task clone command"),
459            },
460            _ => panic!("expected task command"),
461        }
462    }
463
464    #[test]
465    fn task_clone_help_mentions_examples() {
466        let mut cmd = Cli::command();
467        let task = cmd.find_subcommand_mut("task").expect("task subcommand");
468        let clone = task
469            .find_subcommand_mut("clone")
470            .expect("task clone subcommand");
471        let help = clone.render_long_help().to_string();
472
473        assert!(
474            help.contains("ralph task clone RQ-0001"),
475            "missing clone example: {help}"
476        );
477        assert!(
478            help.contains("--status"),
479            "missing --status example: {help}"
480        );
481        assert!(
482            help.contains("--title-prefix"),
483            "missing --title-prefix example: {help}"
484        );
485        assert!(
486            help.contains("ralph task duplicate"),
487            "missing duplicate alias example: {help}"
488        );
489    }
490
491    #[test]
492    fn task_batch_status_parses_multiple_ids() {
493        let cli = Cli::try_parse_from([
494            "ralph", "task", "batch", "status", "doing", "RQ-0001", "RQ-0002", "RQ-0003",
495        ])
496        .expect("parse");
497        match cli.command {
498            crate::cli::Command::Task(args) => match args.command {
499                Some(crate::cli::task::TaskCommand::Batch(args)) => match args.operation {
500                    BatchOperation::Status(status_args) => {
501                        assert_eq!(status_args.status, TaskStatusArg::Doing);
502                        assert_eq!(
503                            status_args.select.task_ids,
504                            vec!["RQ-0001", "RQ-0002", "RQ-0003"]
505                        );
506                        assert!(!args.dry_run);
507                        assert!(!args.continue_on_error);
508                    }
509                    _ => panic!("expected batch status operation"),
510                },
511                _ => panic!("expected task batch command"),
512            },
513            _ => panic!("expected task command"),
514        }
515    }
516
517    #[test]
518    fn task_batch_status_parses_tag_filter() {
519        let cli = Cli::try_parse_from([
520            "ralph",
521            "task",
522            "batch",
523            "status",
524            "doing",
525            "--tag-filter",
526            "rust",
527            "--tag-filter",
528            "cli",
529        ])
530        .expect("parse");
531        match cli.command {
532            crate::cli::Command::Task(args) => match args.command {
533                Some(crate::cli::task::TaskCommand::Batch(args)) => match args.operation {
534                    BatchOperation::Status(status_args) => {
535                        assert_eq!(status_args.status, TaskStatusArg::Doing);
536                        assert!(status_args.select.task_ids.is_empty());
537                        assert_eq!(status_args.select.tag_filter, vec!["rust", "cli"]);
538                    }
539                    _ => panic!("expected batch status operation"),
540                },
541                _ => panic!("expected task batch command"),
542            },
543            _ => panic!("expected task command"),
544        }
545    }
546
547    #[test]
548    fn task_batch_field_parses_multiple_ids() {
549        let cli = Cli::try_parse_from([
550            "ralph", "task", "batch", "field", "severity", "high", "RQ-0001", "RQ-0002",
551        ])
552        .expect("parse");
553        match cli.command {
554            crate::cli::Command::Task(args) => match args.command {
555                Some(crate::cli::task::TaskCommand::Batch(args)) => match args.operation {
556                    BatchOperation::Field(field_args) => {
557                        assert_eq!(field_args.key, "severity");
558                        assert_eq!(field_args.value, "high");
559                        assert_eq!(field_args.select.task_ids, vec!["RQ-0001", "RQ-0002"]);
560                    }
561                    _ => panic!("expected batch field operation"),
562                },
563                _ => panic!("expected task batch command"),
564            },
565            _ => panic!("expected task command"),
566        }
567    }
568
569    #[test]
570    fn task_batch_edit_parses_dry_run() {
571        let cli = Cli::try_parse_from([
572            "ralph",
573            "task",
574            "batch",
575            "--dry-run",
576            "edit",
577            "priority",
578            "high",
579            "RQ-0001",
580            "RQ-0002",
581        ])
582        .expect("parse");
583        match cli.command {
584            crate::cli::Command::Task(args) => match args.command {
585                Some(crate::cli::task::TaskCommand::Batch(args)) => {
586                    assert!(args.dry_run);
587                    assert!(!args.continue_on_error);
588                    match args.operation {
589                        BatchOperation::Edit(edit_args) => {
590                            assert_eq!(edit_args.field, TaskEditFieldArg::Priority);
591                            assert_eq!(edit_args.value, "high");
592                            assert_eq!(edit_args.select.task_ids, vec!["RQ-0001", "RQ-0002"]);
593                        }
594                        _ => panic!("expected batch edit operation"),
595                    }
596                }
597                _ => panic!("expected task batch command"),
598            },
599            _ => panic!("expected task command"),
600        }
601    }
602
603    #[test]
604    fn task_batch_parses_continue_on_error() {
605        let cli = Cli::try_parse_from([
606            "ralph",
607            "task",
608            "batch",
609            "--continue-on-error",
610            "status",
611            "doing",
612            "RQ-0001",
613            "RQ-0002",
614        ])
615        .expect("parse");
616        match cli.command {
617            crate::cli::Command::Task(args) => match args.command {
618                Some(crate::cli::task::TaskCommand::Batch(args)) => {
619                    assert!(!args.dry_run);
620                    assert!(args.continue_on_error);
621                    match args.operation {
622                        BatchOperation::Status(status_args) => {
623                            assert_eq!(status_args.status, TaskStatusArg::Doing);
624                        }
625                        _ => panic!("expected batch status operation"),
626                    }
627                }
628                _ => panic!("expected task batch command"),
629            },
630            _ => panic!("expected task command"),
631        }
632    }
633
634    #[test]
635    fn task_batch_help_mentions_examples() {
636        let mut cmd = Cli::command();
637        let task = cmd.find_subcommand_mut("task").expect("task subcommand");
638        let batch = task
639            .find_subcommand_mut("batch")
640            .expect("task batch subcommand");
641        let help = batch.render_long_help().to_string();
642
643        assert!(
644            help.contains("ralph task batch status doing"),
645            "missing batch status example: {help}"
646        );
647        assert!(
648            help.contains("--tag-filter"),
649            "missing --tag-filter example: {help}"
650        );
651        assert!(
652            help.contains("--dry-run"),
653            "missing --dry-run example: {help}"
654        );
655        assert!(
656            help.contains("--continue-on-error"),
657            "missing --continue-on-error example: {help}"
658        );
659    }
660
661    #[test]
662    fn task_status_parses_multiple_ids() {
663        let cli = Cli::try_parse_from([
664            "ralph", "task", "status", "doing", "RQ-0001", "RQ-0002", "RQ-0003",
665        ])
666        .expect("parse");
667        match cli.command {
668            crate::cli::Command::Task(args) => match args.command {
669                Some(crate::cli::task::TaskCommand::Status(args)) => {
670                    assert_eq!(args.status, TaskStatusArg::Doing);
671                    assert_eq!(args.task_ids, vec!["RQ-0001", "RQ-0002", "RQ-0003"]);
672                }
673                _ => panic!("expected task status command"),
674            },
675            _ => panic!("expected task command"),
676        }
677    }
678
679    #[test]
680    fn task_status_parses_tag_filter() {
681        let cli = Cli::try_parse_from([
682            "ralph",
683            "task",
684            "status",
685            "doing",
686            "--tag-filter",
687            "rust",
688            "--tag-filter",
689            "cli",
690        ])
691        .expect("parse");
692        match cli.command {
693            crate::cli::Command::Task(args) => match args.command {
694                Some(crate::cli::task::TaskCommand::Status(args)) => {
695                    assert_eq!(args.status, TaskStatusArg::Doing);
696                    assert!(args.task_ids.is_empty());
697                    assert_eq!(args.tag_filter, vec!["rust", "cli"]);
698                }
699                _ => panic!("expected task status command"),
700            },
701            _ => panic!("expected task command"),
702        }
703    }
704
705    #[test]
706    fn task_field_parses_multiple_ids() {
707        let cli = Cli::try_parse_from([
708            "ralph", "task", "field", "severity", "high", "RQ-0001", "RQ-0002",
709        ])
710        .expect("parse");
711        match cli.command {
712            crate::cli::Command::Task(args) => match args.command {
713                Some(crate::cli::task::TaskCommand::Field(args)) => {
714                    assert_eq!(args.key, "severity");
715                    assert_eq!(args.value, "high");
716                    assert_eq!(args.task_ids, vec!["RQ-0001", "RQ-0002"]);
717                }
718                _ => panic!("expected task field command"),
719            },
720            _ => panic!("expected task command"),
721        }
722    }
723
724    #[test]
725    fn task_field_parses_dry_run_flag() {
726        let cli = Cli::try_parse_from([
727            "ralph",
728            "task",
729            "field",
730            "--dry-run",
731            "severity",
732            "high",
733            "RQ-0001",
734        ])
735        .expect("parse");
736        match cli.command {
737            crate::cli::Command::Task(args) => match args.command {
738                Some(crate::cli::task::TaskCommand::Field(args)) => {
739                    assert!(args.dry_run);
740                    assert_eq!(args.key, "severity");
741                    assert_eq!(args.value, "high");
742                    assert_eq!(args.task_ids, vec!["RQ-0001"]);
743                }
744                _ => panic!("expected task field command"),
745            },
746            _ => panic!("expected task command"),
747        }
748    }
749
750    #[test]
751    fn task_field_without_dry_run_defaults_to_false() {
752        let cli = Cli::try_parse_from(["ralph", "task", "field", "severity", "high", "RQ-0001"])
753            .expect("parse");
754        match cli.command {
755            crate::cli::Command::Task(args) => match args.command {
756                Some(crate::cli::task::TaskCommand::Field(args)) => {
757                    assert!(!args.dry_run);
758                }
759                _ => panic!("expected task field command"),
760            },
761            _ => panic!("expected task command"),
762        }
763    }
764
765    #[test]
766    fn task_edit_parses_multiple_ids() {
767        let cli = Cli::try_parse_from([
768            "ralph", "task", "edit", "priority", "high", "RQ-0001", "RQ-0002",
769        ])
770        .expect("parse");
771        match cli.command {
772            crate::cli::Command::Task(args) => match args.command {
773                Some(crate::cli::task::TaskCommand::Edit(args)) => {
774                    assert_eq!(args.field, TaskEditFieldArg::Priority);
775                    assert_eq!(args.value, "high");
776                    assert_eq!(args.task_ids, vec!["RQ-0001", "RQ-0002"]);
777                }
778                _ => panic!("expected task edit command"),
779            },
780            _ => panic!("expected task command"),
781        }
782    }
783}