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