1use crate::config;
27use crate::constants::paths::{
28 SCAN_OVERRIDE_PATH, TASK_BUILDER_OVERRIDE_PATH, WORKER_OVERRIDE_PATH,
29};
30use crate::contracts::ProjectType;
31use crate::promptflow::{self, PromptPolicy};
32use crate::prompts_internal::management as prompt_mgmt;
33use crate::{prompts, queue};
34use anyhow::{Context, Result, bail};
35use std::fs;
36use std::path::{Path, PathBuf};
37
38pub fn list_prompts(repo_root: &Path) -> Result<()> {
40 let templates = prompt_mgmt::list_templates(repo_root);
41
42 println!("Available prompt templates ({} total):\n", templates.len());
43
44 let max_name_len = templates.iter().map(|t| t.name.len()).max().unwrap_or(0);
46
47 for t in templates {
48 let status = if t.has_override { " [override]" } else { "" };
49 println!(
50 " {:width$} {}{}",
51 t.name,
52 t.description,
53 status,
54 width = max_name_len
55 );
56 }
57
58 println!("\nOverride paths: .ralph/prompts/<name>.md");
59 println!("Use 'ralph prompt show <name> --raw' to view raw embedded content");
60
61 Ok(())
62}
63
64pub fn show_prompt(repo_root: &Path, name: &str, raw: bool) -> Result<()> {
66 let id = prompt_mgmt::parse_template_name(name)
67 .ok_or_else(|| anyhow::anyhow!("Unknown template name: '{}'", name))?;
68
69 let content = if raw {
70 prompt_mgmt::get_embedded_content(id).to_string()
71 } else {
72 prompt_mgmt::get_effective_content(repo_root, id)?
73 };
74
75 print!("{}", content);
76 Ok(())
77}
78
79pub fn export_prompts(repo_root: &Path, name: Option<&str>, force: bool) -> Result<()> {
81 let ralph_version = env!("CARGO_PKG_VERSION");
82
83 if let Some(n) = name {
84 let id = prompt_mgmt::parse_template_name(n)
86 .ok_or_else(|| anyhow::anyhow!("Unknown template name: '{}'", n))?;
87
88 let file_name = prompt_mgmt::template_file_name(id);
89 let written = prompt_mgmt::export_template(repo_root, id, force, ralph_version)?;
90
91 if written {
92 println!("Exported {} to .ralph/prompts/{}.md", file_name, file_name);
93 } else {
94 println!(
95 "Skipped {}: file already exists (use --force to overwrite)",
96 file_name
97 );
98 }
99 } else {
100 let templates = prompt_mgmt::all_template_ids();
102 let mut exported = 0;
103 let mut skipped = 0;
104
105 for id in templates {
106 let file_name = prompt_mgmt::template_file_name(id);
107 match prompt_mgmt::export_template(repo_root, id, force, ralph_version) {
108 Ok(written) => {
109 if written {
110 exported += 1;
111 println!("Exported {}", file_name);
112 } else {
113 skipped += 1;
114 println!("Skipped {}: already exists", file_name);
115 }
116 }
117 Err(e) => {
118 eprintln!("Error exporting {}: {}", file_name, e);
119 }
120 }
121 }
122
123 println!("\nExported {} templates, skipped {}", exported, skipped);
124 if skipped > 0 && !force {
125 println!("Use --force to overwrite existing files");
126 }
127 }
128
129 Ok(())
130}
131
132pub fn sync_prompts(repo_root: &Path, dry_run: bool, force: bool) -> Result<()> {
134 let ralph_version = env!("CARGO_PKG_VERSION");
135 let templates = prompt_mgmt::all_template_ids();
136
137 let mut up_to_date = Vec::new();
138 let mut outdated = Vec::new();
139 let mut user_modified = Vec::new();
140 let mut missing = Vec::new();
141
142 for id in &templates {
144 let file_name = prompt_mgmt::template_file_name(*id);
145 let status = prompt_mgmt::check_sync_status(repo_root, *id)?;
146
147 match status {
148 prompt_mgmt::SyncStatus::UpToDate => up_to_date.push(file_name),
149 prompt_mgmt::SyncStatus::Outdated => outdated.push((file_name, *id)),
150 prompt_mgmt::SyncStatus::UserModified => user_modified.push((file_name, *id)),
151 prompt_mgmt::SyncStatus::Unknown => user_modified.push((file_name, *id)),
152 prompt_mgmt::SyncStatus::Missing => missing.push((file_name, *id)),
153 }
154 }
155
156 if dry_run {
157 println!("Dry run - no changes will be made:\n");
158
159 if !outdated.is_empty() {
160 println!("Would update ({}):", outdated.len());
161 for (name, _) in &outdated {
162 println!(" {}", name);
163 }
164 }
165
166 if !missing.is_empty() {
167 println!("Would create ({}):", missing.len());
168 for (name, _) in &missing {
169 println!(" {}", name);
170 }
171 }
172
173 if !user_modified.is_empty() {
174 println!("Would skip (user modified) ({}):", user_modified.len());
175 for (name, _) in &user_modified {
176 println!(" {}", name);
177 }
178 }
179
180 if !up_to_date.is_empty() {
181 println!("Up to date ({}):", up_to_date.len());
182 for name in &up_to_date {
183 println!(" {}", name);
184 }
185 }
186
187 return Ok(());
188 }
189
190 let mut updated = 0;
192 let mut skipped = 0;
193 let mut created = 0;
194
195 for (name, id) in outdated {
197 match prompt_mgmt::export_template(repo_root, id, true, ralph_version) {
198 Ok(_) => {
199 println!("Updated {} (outdated)", name);
200 updated += 1;
201 }
202 Err(e) => {
203 eprintln!("Error updating {}: {}", name, e);
204 skipped += 1;
205 }
206 }
207 }
208
209 for (name, id) in missing {
211 match prompt_mgmt::export_template(repo_root, id, false, ralph_version) {
212 Ok(_) => {
213 println!("Created {}", name);
214 created += 1;
215 }
216 Err(e) => {
217 eprintln!("Error creating {}: {}", name, e);
218 skipped += 1;
219 }
220 }
221 }
222
223 for (name, id) in user_modified {
225 if force {
226 match prompt_mgmt::export_template(repo_root, id, true, ralph_version) {
227 Ok(_) => {
228 println!("Overwrote {} (user modified, --force)", name);
229 updated += 1;
230 }
231 Err(e) => {
232 eprintln!("Error overwriting {}: {}", name, e);
233 skipped += 1;
234 }
235 }
236 } else {
237 println!("Skipped {} (user modified, use --force to overwrite)", name);
238 skipped += 1;
239 }
240 }
241
242 println!(
243 "\nSync complete: {} updated, {} created, {} skipped",
244 updated, created, skipped
245 );
246
247 Ok(())
248}
249
250pub fn diff_prompt(repo_root: &Path, name: &str) -> Result<()> {
252 let id = prompt_mgmt::parse_template_name(name)
253 .ok_or_else(|| anyhow::anyhow!("Unknown template name: '{}'", name))?;
254
255 match prompt_mgmt::generate_diff(repo_root, id)? {
256 Some(diff) => {
257 print!("{}", diff);
258 }
259 None => {
260 println!("No local override for '{}' - using embedded default", name);
261 }
262 }
263
264 Ok(())
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum WorkerMode {
269 Phase1,
271 Phase2,
273 Phase3,
275 Single,
277}
278
279#[derive(Debug, Clone)]
280pub struct WorkerPromptOptions {
281 pub task_id: Option<String>,
283 pub mode: WorkerMode,
284 pub repoprompt_plan_required: bool,
286 pub repoprompt_tool_injection: bool,
288 pub iterations: u8,
290 pub iteration_index: u8,
292
293 pub plan_file: Option<PathBuf>,
296 pub plan_text: Option<String>,
298
299 pub explain: bool,
301}
302
303#[derive(Debug, Clone)]
304pub struct ScanPromptOptions {
305 pub focus: String,
306 pub mode: crate::cli::scan::ScanMode,
307 pub repoprompt_tool_injection: bool,
308 pub explain: bool,
309}
310
311#[derive(Debug, Clone)]
312pub struct TaskBuilderPromptOptions {
313 pub request: String,
314 pub hint_tags: String,
315 pub hint_scope: String,
316 pub repoprompt_tool_injection: bool,
317 pub explain: bool,
318}
319
320fn worker_template_source(repo_root: &Path) -> &'static str {
321 if repo_root.join(WORKER_OVERRIDE_PATH).exists() {
322 WORKER_OVERRIDE_PATH
323 } else {
324 "(embedded default)"
325 }
326}
327
328fn scan_template_source(repo_root: &Path) -> &'static str {
329 if repo_root.join(SCAN_OVERRIDE_PATH).exists() {
330 SCAN_OVERRIDE_PATH
331 } else {
332 "(embedded default)"
333 }
334}
335
336fn task_builder_template_source(repo_root: &Path) -> &'static str {
337 if repo_root.join(TASK_BUILDER_OVERRIDE_PATH).exists() {
338 TASK_BUILDER_OVERRIDE_PATH
339 } else {
340 "(embedded default)"
341 }
342}
343
344fn resolve_worker_task_id(resolved: &config::Resolved, task_id: Option<String>) -> Result<String> {
349 if let Some(id) = task_id {
350 let trimmed = id.trim();
351 if trimmed.is_empty() {
352 bail!("--task-id was provided but is empty");
353 }
354 return Ok(trimmed.to_string());
355 }
356
357 if resolved.queue_path.exists() {
360 let queue_file = queue::load_queue(&resolved.queue_path)
361 .with_context(|| format!("read {}", resolved.queue_path.display()))?;
362
363 let done_file = if resolved.done_path.exists() {
364 Some(
365 queue::load_queue(&resolved.done_path)
366 .with_context(|| format!("read {}", resolved.done_path.display()))?,
367 )
368 } else {
369 None
370 };
371
372 let options = queue::operations::RunnableSelectionOptions::new(false, true);
373 if let Some(idx) =
374 queue::operations::select_runnable_task_index(&queue_file, done_file.as_ref(), options)
375 && let Some(task) = queue_file.tasks.get(idx)
376 {
377 return Ok(task.id.trim().to_string());
378 }
379 }
380
381 bail!(
382 "No doing/todo tasks found to infer a worker task id. Provide --task-id (e.g., RQ-0001) to preview the worker prompt."
383 );
384}
385
386fn load_plan_text_for_phase2(
393 repo_root: &Path,
394 task_id: &str,
395 plan_text: Option<String>,
396 plan_file: Option<PathBuf>,
397) -> Result<String> {
398 if let Some(text) = plan_text {
399 let trimmed = text.trim();
400 if trimmed.is_empty() {
401 bail!("--plan-text was provided but is empty");
402 }
403 return Ok(trimmed.to_string());
404 }
405
406 if let Some(path) = plan_file {
407 let raw = fs::read_to_string(&path)
408 .with_context(|| format!("read plan file {}", path.display()))?;
409 let trimmed = raw.trim();
410 if trimmed.is_empty() {
411 bail!("Plan file is empty: {}", path.display());
412 }
413 return Ok(trimmed.to_string());
414 }
415
416 match promptflow::read_plan_cache(repo_root, task_id) {
419 Ok(plan) => Ok(plan),
420 Err(_) => {
421 let cache_path = promptflow::plan_cache_path(repo_root, task_id);
422 Ok(format!(
423 "*No plan file found*\n\nNo plan file was found at {}. Please proceed with implementation based on the task requirements.",
424 cache_path.display()
425 ))
426 }
427 }
428}
429
430fn load_phase2_final_response_for_phase3(repo_root: &Path, task_id: &str) -> String {
431 match promptflow::read_phase2_final_response_cache(repo_root, task_id) {
432 Ok(text) => text,
433 Err(err) => {
434 log::warn!(
435 "Phase 2 final response cache unavailable for {}: {}",
436 task_id,
437 err
438 );
439 "(Phase 2 final response unavailable; cache missing.)".to_string()
440 }
441 }
442}
443
444pub fn build_worker_prompt(
445 resolved: &config::Resolved,
446 opts: WorkerPromptOptions,
447) -> Result<String> {
448 let task_id = resolve_worker_task_id(resolved, opts.task_id)?;
449 if opts.iterations == 0 {
450 bail!("--iterations must be >= 1");
451 }
452 if opts.iteration_index == 0 {
453 bail!("--iteration-index must be >= 1");
454 }
455 if opts.iteration_index > opts.iterations {
456 bail!(
457 "--iteration-index ({}) cannot exceed --iterations ({})",
458 opts.iteration_index,
459 opts.iterations
460 );
461 }
462
463 let template = prompts::load_worker_prompt(&resolved.repo_root)?;
464 let project_type = resolved.config.project_type.unwrap_or(ProjectType::Code);
465 let base_prompt =
466 prompts::render_worker_prompt(&template, &task_id, project_type, &resolved.config)?;
467 let base_prompt =
468 prompts::wrap_with_instruction_files(&resolved.repo_root, &base_prompt, &resolved.config)?;
469
470 let policy = PromptPolicy {
471 repoprompt_plan_required: opts.repoprompt_plan_required,
472 repoprompt_tool_injection: opts.repoprompt_tool_injection,
473 };
474 let is_followup = opts.iteration_index > 1;
475 let is_final_iteration = opts.iteration_index == opts.iterations;
476 let iteration_context = if is_followup {
477 prompts::ITERATION_CONTEXT_REFINEMENT
478 } else {
479 ""
480 };
481 let iteration_completion_block = if is_final_iteration {
482 ""
483 } else {
484 prompts::ITERATION_COMPLETION_BLOCK
485 };
486 let phase3_completion_guidance = if is_final_iteration {
487 prompts::PHASE3_COMPLETION_GUIDANCE_FINAL
488 } else {
489 prompts::PHASE3_COMPLETION_GUIDANCE_NONFINAL
490 };
491
492 let configured_phases = resolved.config.agent.phases.unwrap_or(2);
493 let total_phases = match opts.mode {
494 WorkerMode::Phase3 => 3,
495 WorkerMode::Single => 1,
496 _ => configured_phases.clamp(2, 3),
497 };
498
499 let load_completion_checklist = || -> Result<String> {
500 let template = prompts::load_completion_checklist(&resolved.repo_root)?;
501 prompts::render_completion_checklist(&template, &task_id, &resolved.config, false)
502 };
503
504 let prompt = match opts.mode {
505 WorkerMode::Phase1 => {
506 let phase1_template = prompts::load_worker_phase1_prompt(&resolved.repo_root)?;
507 promptflow::build_phase1_prompt(
508 &phase1_template,
509 &base_prompt,
510 iteration_context,
511 promptflow::PHASE1_TASK_REFRESH_REQUIRED_INSTRUCTION,
512 &task_id,
513 total_phases,
514 &policy,
515 &resolved.config,
516 )?
517 }
518 WorkerMode::Phase2 => {
519 let plan_text = load_plan_text_for_phase2(
520 &resolved.repo_root,
521 &task_id,
522 opts.plan_text,
523 opts.plan_file,
524 )?;
525 if total_phases == 3 {
526 let handoff_template = prompts::load_phase2_handoff_checklist(&resolved.repo_root)?;
527 let handoff_checklist =
528 prompts::render_phase2_handoff_checklist(&handoff_template, &resolved.config)?;
529 let phase2_template =
530 prompts::load_worker_phase2_handoff_prompt(&resolved.repo_root)?;
531 promptflow::build_phase2_handoff_prompt(
532 &phase2_template,
533 &base_prompt,
534 &plan_text,
535 &handoff_checklist,
536 iteration_context,
537 iteration_completion_block,
538 &task_id,
539 total_phases,
540 &policy,
541 &resolved.config,
542 )?
543 } else {
544 let completion_checklist = load_completion_checklist()?;
545 let phase2_template = prompts::load_worker_phase2_prompt(&resolved.repo_root)?;
546 promptflow::build_phase2_prompt(
547 &phase2_template,
548 &base_prompt,
549 &plan_text,
550 &completion_checklist,
551 iteration_context,
552 iteration_completion_block,
553 &task_id,
554 total_phases,
555 &policy,
556 &resolved.config,
557 )?
558 }
559 }
560 WorkerMode::Phase3 => {
561 let review_template = prompts::load_code_review_prompt(&resolved.repo_root)?;
562 let review_body = prompts::render_code_review_prompt(
563 &review_template,
564 &task_id,
565 project_type,
566 &resolved.config,
567 )?;
568 let completion_checklist = load_completion_checklist()?;
569 let phase3_template = prompts::load_worker_phase3_prompt(&resolved.repo_root)?;
570 let phase2_final_response =
571 load_phase2_final_response_for_phase3(&resolved.repo_root, &task_id);
572 promptflow::build_phase3_prompt(
573 &phase3_template,
574 &base_prompt,
575 &review_body,
576 &phase2_final_response,
577 &task_id,
578 &completion_checklist,
579 iteration_context,
580 iteration_completion_block,
581 phase3_completion_guidance,
582 total_phases,
583 &policy,
584 &resolved.config,
585 )?
586 }
587 WorkerMode::Single => {
588 let completion_checklist = load_completion_checklist()?;
589 let single_template = prompts::load_worker_single_phase_prompt(&resolved.repo_root)?;
590 promptflow::build_single_phase_prompt(
591 &single_template,
592 &base_prompt,
593 &completion_checklist,
594 iteration_context,
595 iteration_completion_block,
596 &task_id,
597 &policy,
598 &resolved.config,
599 )?
600 }
601 };
602
603 if !opts.explain {
604 return Ok(prompt);
605 }
606
607 let mut header = String::new();
608 header.push_str("# RALPH PROMPT PREVIEW (worker)\n\n");
609 header.push_str(&format!("- task_id: {}\n", task_id));
610 header.push_str(&format!(
611 "- mode: {}\n",
612 match opts.mode {
613 WorkerMode::Phase1 => "phase1",
614 WorkerMode::Phase2 => "phase2",
615 WorkerMode::Phase3 => "phase3",
616 WorkerMode::Single => "single",
617 }
618 ));
619 header.push_str(&format!(
620 "- repoprompt_plan_required: {}\n",
621 opts.repoprompt_plan_required
622 ));
623 header.push_str(&format!(
624 "- repoprompt_tool_injection: {}\n",
625 opts.repoprompt_tool_injection
626 ));
627 header.push_str(&format!(
628 "- iteration: {}/{}\n",
629 opts.iteration_index, opts.iterations
630 ));
631 header.push_str(&format!(
632 "- worker template source: {}\n",
633 worker_template_source(&resolved.repo_root)
634 ));
635 header.push_str("\n---\n\n");
636
637 Ok(format!("{header}{prompt}"))
638}
639
640pub fn build_scan_prompt(resolved: &config::Resolved, opts: ScanPromptOptions) -> Result<String> {
641 let scan_version = resolved
642 .config
643 .agent
644 .scan_prompt_version
645 .unwrap_or_default();
646 let template = prompts::load_scan_prompt(&resolved.repo_root, scan_version, opts.mode)?;
647 let project_type = resolved.config.project_type.unwrap_or(ProjectType::Code);
648 let rendered = prompts::render_scan_prompt(
649 &template,
650 &opts.focus,
651 opts.mode,
652 scan_version,
653 project_type,
654 &resolved.config,
655 )?;
656 let prompt =
657 prompts::wrap_with_repoprompt_requirement(&rendered, opts.repoprompt_tool_injection);
658 let prompt =
659 prompts::wrap_with_instruction_files(&resolved.repo_root, &prompt, &resolved.config)?;
660
661 if !opts.explain {
662 return Ok(prompt);
663 }
664
665 let mut header = String::new();
666 header.push_str("# RALPH PROMPT PREVIEW (scan)\n\n");
667 header.push_str(&format!(
668 "- focus: {}\n",
669 if opts.focus.trim().is_empty() {
670 "(none)"
671 } else {
672 opts.focus.trim()
673 }
674 ));
675 header.push_str(&format!(
676 "- repoprompt_tool_injection: {}\n",
677 opts.repoprompt_tool_injection
678 ));
679 header.push_str(&format!(
680 "- scan template source: {}\n",
681 scan_template_source(&resolved.repo_root)
682 ));
683 header.push_str("\n---\n\n");
684
685 Ok(format!("{header}{prompt}"))
686}
687
688pub fn build_task_builder_prompt(
689 resolved: &config::Resolved,
690 opts: TaskBuilderPromptOptions,
691) -> Result<String> {
692 let request = opts.request.trim();
693 if request.is_empty() {
694 bail!("Missing request: task builder prompt preview requires a non-empty request.");
695 }
696
697 let template = prompts::load_task_builder_prompt(&resolved.repo_root)?;
698 let project_type = resolved.config.project_type.unwrap_or(ProjectType::Code);
699 let rendered = prompts::render_task_builder_prompt(
700 &template,
701 request,
702 &opts.hint_tags,
703 &opts.hint_scope,
704 project_type,
705 &resolved.config,
706 )?;
707 let prompt =
708 prompts::wrap_with_repoprompt_requirement(&rendered, opts.repoprompt_tool_injection);
709 let prompt =
710 prompts::wrap_with_instruction_files(&resolved.repo_root, &prompt, &resolved.config)?;
711
712 if !opts.explain {
713 return Ok(prompt);
714 }
715
716 let mut header = String::new();
717 header.push_str("# RALPH PROMPT PREVIEW (task builder)\n\n");
718 header.push_str(&format!("- request: {}\n", request));
719 header.push_str(&format!(
720 "- hint_tags: {}\n",
721 if opts.hint_tags.trim().is_empty() {
722 "(empty)"
723 } else {
724 opts.hint_tags.trim()
725 }
726 ));
727 header.push_str(&format!(
728 "- hint_scope: {}\n",
729 if opts.hint_scope.trim().is_empty() {
730 "(empty)"
731 } else {
732 opts.hint_scope.trim()
733 }
734 ));
735 header.push_str(&format!(
736 "- repoprompt_tool_injection: {}\n",
737 opts.repoprompt_tool_injection
738 ));
739 header.push_str(&format!(
740 "- task builder template source: {}\n",
741 task_builder_template_source(&resolved.repo_root)
742 ));
743 header.push_str("\n---\n\n");
744
745 Ok(format!("{header}{prompt}"))
746}
747
748#[cfg(test)]
749mod tests {
750 use super::resolve_worker_task_id;
751 use crate::config::Resolved;
752 use crate::contracts::{Config, QueueFile, Task, TaskPriority, TaskStatus};
753 use crate::queue;
754 use tempfile::TempDir;
755
756 fn make_task(id: &str, status: TaskStatus) -> Task {
757 Task {
758 id: id.to_string(),
759 title: format!("Task {id}"),
760 description: None,
761 status,
762 priority: TaskPriority::Medium,
763 tags: vec!["test".to_string()],
764 scope: vec!["crates/ralph".to_string()],
765 evidence: vec!["test".to_string()],
766 plan: vec!["plan".to_string()],
767 notes: vec![],
768 request: Some("request".to_string()),
769 agent: None,
770 created_at: Some("2026-01-18T00:00:00Z".to_string()),
771 updated_at: Some("2026-01-18T00:00:00Z".to_string()),
772 completed_at: None,
773 started_at: None,
774 scheduled_start: None,
775 depends_on: vec![],
776 blocks: vec![],
777 relates_to: vec![],
778 duplicates: None,
779 custom_fields: std::collections::HashMap::new(),
780 estimated_minutes: None,
781 actual_minutes: None,
782 parent_id: None,
783 }
784 }
785
786 fn make_resolved(temp: &TempDir) -> Resolved {
787 let repo_root = temp.path().to_path_buf();
788 let queue_path = repo_root.join("queue.json");
789 let done_path = repo_root.join("done.json");
790 Resolved {
791 config: Config::default(),
792 repo_root,
793 queue_path,
794 done_path,
795 id_prefix: "RQ".to_string(),
796 id_width: 4,
797 global_config_path: None,
798 project_config_path: None,
799 }
800 }
801
802 #[test]
803 fn resolve_worker_task_id_trims_explicit_task_id() {
804 let temp = TempDir::new().expect("tempdir");
805 let resolved = make_resolved(&temp);
806 let id = resolve_worker_task_id(&resolved, Some(" RQ-0009 ".to_string()))
807 .expect("should trim");
808 assert_eq!(id, "RQ-0009");
809 }
810
811 #[test]
812 fn resolve_worker_task_id_prefers_doing() {
813 let temp = TempDir::new().expect("tempdir");
814 let resolved = make_resolved(&temp);
815 let queue = QueueFile {
816 version: 1,
817 tasks: vec![
818 make_task("RQ-0001", TaskStatus::Todo),
819 make_task("RQ-0002", TaskStatus::Doing),
820 ],
821 };
822 queue::save_queue(&resolved.queue_path, &queue).expect("save queue");
823
824 let id = resolve_worker_task_id(&resolved, None).expect("should resolve doing");
825 assert_eq!(id, "RQ-0002");
826 }
827
828 #[test]
829 fn resolve_worker_task_id_returns_runnable_todo() {
830 let temp = TempDir::new().expect("tempdir");
831 let resolved = make_resolved(&temp);
832
833 let mut todo = make_task("RQ-0003", TaskStatus::Todo);
834 todo.depends_on = vec!["RQ-0002".to_string()];
835
836 let queue = QueueFile {
837 version: 1,
838 tasks: vec![todo],
839 };
840 let done = QueueFile {
841 version: 1,
842 tasks: vec![make_task("RQ-0002", TaskStatus::Done)],
843 };
844 queue::save_queue(&resolved.queue_path, &queue).expect("save queue");
845 queue::save_queue(&resolved.done_path, &done).expect("save done");
846
847 let id = resolve_worker_task_id(&resolved, None).expect("should resolve todo");
848 assert_eq!(id, "RQ-0003");
849 }
850}