1use crate::cli::machine::MachineQueueUndoArgs;
13use crate::config;
14use anyhow::Result;
15use clap::Args;
16
17#[derive(Args, Debug)]
18pub struct UndoArgs {
19 #[arg(long, short)]
21 pub id: Option<String>,
22
23 #[arg(long)]
25 pub list: bool,
26
27 #[arg(long)]
29 pub dry_run: bool,
30
31 #[arg(long, short)]
33 pub verbose: bool,
34}
35
36pub fn handle(args: UndoArgs, force: bool) -> Result<()> {
38 let resolved = config::resolve_from_cwd()?;
39 let document = crate::cli::machine::build_queue_undo_document(
40 &resolved,
41 force,
42 &MachineQueueUndoArgs {
43 id: args.id.clone(),
44 list: args.list,
45 dry_run: args.dry_run,
46 },
47 )?;
48
49 println!("{}", document.continuation.headline);
50 println!("{}", document.continuation.detail);
51
52 if let Some(blocking) = document
53 .blocking
54 .as_ref()
55 .or(document.continuation.blocking.as_ref())
56 {
57 println!();
58 println!(
59 "Operator state: {}",
60 format!("{:?}", blocking.status).to_lowercase()
61 );
62 println!("{}", blocking.message);
63 if !blocking.detail.is_empty() {
64 println!("{}", blocking.detail);
65 }
66 }
67
68 if let Some(result) = &document.result {
69 println!();
70 if args.list {
71 let snapshots =
72 serde_json::from_value::<Vec<crate::undo::UndoSnapshotMeta>>(result.clone())?;
73 if snapshots.is_empty() {
74 println!(
75 "Ralph will create new checkpoints automatically before future queue writes."
76 );
77 } else {
78 println!("Available continuation checkpoints (newest first):");
79 println!();
80 for (index, snapshot) in snapshots.iter().enumerate() {
81 println!(
82 " {}. {} [{}]",
83 index + 1,
84 snapshot.operation,
85 snapshot.timestamp
86 );
87 println!(" ID: {}", snapshot.id);
88 }
89 }
90 } else {
91 let restore = serde_json::from_value::<crate::undo::RestoreResult>(result.clone())?;
92 println!("Checkpoint: {}", restore.snapshot_id);
93 println!("Operation: {}", restore.operation);
94 println!("Timestamp: {}", restore.timestamp);
95 println!("Tasks affected: {}", restore.tasks_affected);
96 if args.verbose && !args.dry_run {
97 println!();
98 println!("Run `ralph queue list` to inspect the restored queue state in detail.");
99 }
100 }
101 }
102
103 if !document.continuation.next_steps.is_empty() {
104 println!();
105 println!("Next:");
106 for (index, step) in document.continuation.next_steps.iter().enumerate() {
107 println!(" {}. {} — {}", index + 1, step.command, step.detail);
108 }
109 }
110
111 Ok(())
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::contracts::{QueueFile, Task, TaskStatus};
118 use std::collections::HashMap;
119 use tempfile::TempDir;
120
121 fn create_test_resolved(temp_dir: &TempDir) -> config::Resolved {
122 let repo_root = temp_dir.path();
123 let ralph_dir = repo_root.join(".ralph");
124 std::fs::create_dir_all(&ralph_dir).unwrap();
125
126 let queue_path = ralph_dir.join("queue.json");
127 let done_path = ralph_dir.join("done.json");
128
129 let queue = QueueFile {
130 version: 1,
131 tasks: vec![Task {
132 id: "RQ-0001".to_string(),
133 title: "Test task".to_string(),
134 status: TaskStatus::Todo,
135 description: None,
136 priority: Default::default(),
137 tags: vec!["test".to_string()],
138 scope: vec!["crates/ralph".to_string()],
139 evidence: vec!["observed".to_string()],
140 plan: vec!["do thing".to_string()],
141 notes: vec![],
142 request: Some("test request".to_string()),
143 agent: None,
144 created_at: Some("2026-01-18T00:00:00Z".to_string()),
145 updated_at: Some("2026-01-18T00:00:00Z".to_string()),
146 completed_at: None,
147 started_at: None,
148 scheduled_start: None,
149 estimated_minutes: None,
150 actual_minutes: None,
151 depends_on: vec![],
152 blocks: vec![],
153 relates_to: vec![],
154 duplicates: None,
155 custom_fields: HashMap::new(),
156 parent_id: None,
157 }],
158 };
159
160 crate::queue::save_queue(&queue_path, &queue).unwrap();
161
162 config::Resolved {
163 config: crate::contracts::Config::default(),
164 repo_root: repo_root.to_path_buf(),
165 queue_path,
166 done_path,
167 id_prefix: "RQ".to_string(),
168 id_width: 4,
169 global_config_path: None,
170 project_config_path: None,
171 }
172 }
173
174 #[test]
175 fn build_undo_list_document_shows_snapshots() {
176 let temp = TempDir::new().unwrap();
177 let resolved = create_test_resolved(&temp);
178
179 crate::undo::create_undo_snapshot(&resolved, "test operation").unwrap();
180
181 let document = crate::cli::machine::build_queue_undo_document(
182 &resolved,
183 false,
184 &MachineQueueUndoArgs {
185 id: None,
186 list: true,
187 dry_run: false,
188 },
189 )
190 .expect("undo list document");
191 assert!(document.result.is_some());
192 }
193
194 #[test]
195 fn build_undo_list_document_handles_empty_snapshots() {
196 let temp = TempDir::new().unwrap();
197 let resolved = create_test_resolved(&temp);
198
199 let document = crate::cli::machine::build_queue_undo_document(
200 &resolved,
201 false,
202 &MachineQueueUndoArgs {
203 id: None,
204 list: true,
205 dry_run: false,
206 },
207 )
208 .expect("undo list document");
209 assert!(document.result.is_some());
210 }
211}