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