1use crate::banner::{print_final_summary, PipelineSummary};
11use crate::checkpoint::clear_checkpoint_with_workspace;
12use crate::config::Config;
13use crate::files::protection::monitoring::PromptMonitor;
14use crate::logger::Colors;
15use crate::logger::Logger;
16use crate::pipeline::AgentPhaseGuard;
17use crate::pipeline::Timer;
18use crate::reducer::state::PipelineState;
19use crate::workspace::Workspace;
20
21#[derive(Copy, Clone)]
23pub struct FinalizeContext<'a> {
24 pub logger: &'a Logger,
25 pub colors: Colors,
26 pub config: &'a Config,
27 pub timer: &'a Timer,
28 pub workspace: &'a dyn Workspace,
29}
30
31#[must_use]
41pub const fn build_pipeline_summary(
42 total_time: String,
43 config: &Config,
44 final_state: &PipelineState,
45) -> PipelineSummary {
46 PipelineSummary {
47 total_time,
48 dev_runs_completed: final_state.metrics.dev_iterations_completed as usize,
49 dev_runs_total: final_state.metrics.max_dev_iterations as usize,
50 review_passes_completed: final_state.metrics.review_passes_completed as usize,
51 review_passes_total: final_state.metrics.max_review_passes as usize,
52 review_runs: final_state.metrics.review_runs_total as usize,
53 changes_detected: final_state.metrics.commits_created_total as usize,
54 isolation_mode: config.isolation_mode,
55 verbose: config.verbosity.is_verbose(),
56 review_summary: None,
57 }
58}
59
60pub fn finalize_pipeline(
61 agent_phase_guard: &mut AgentPhaseGuard<'_>,
62 ctx: FinalizeContext<'_>,
63 final_state: &PipelineState,
64 prompt_monitor: Option<PromptMonitor>,
65) {
66 if let Some(monitor) = prompt_monitor {
68 for warning in monitor.stop() {
69 ctx.logger.warn(&warning);
70 }
71 }
72
73 let repo_root = ctx.workspace.root();
75 crate::git_helpers::end_agent_phase_in_repo(repo_root);
76 crate::git_helpers::disable_git_wrapper(agent_phase_guard.git_helpers);
77 let mut cleanup_ok = true;
78
79 if let Err(err) = crate::git_helpers::uninstall_hooks_in_repo(repo_root, ctx.logger) {
80 if err.kind() == std::io::ErrorKind::NotFound {
81 ctx.logger.warn(&format!(
85 "Skipping hook uninstall (repo not present on filesystem): {err}"
86 ));
87 } else {
88 cleanup_ok = false;
89 ctx.logger
90 .warn(&format!("Failed to uninstall Ralph hooks: {err}"));
91 }
92 }
93
94 let wrapper_remaining = crate::git_helpers::verify_wrapper_cleaned(repo_root);
95 if !wrapper_remaining.is_empty() {
96 cleanup_ok = false;
97 ctx.logger.warn(&format!(
98 "Wrapper artifacts still present after cleanup: {}",
99 wrapper_remaining.join(", ")
100 ));
101 }
102
103 match crate::git_helpers::verify_hooks_removed(repo_root) {
104 Ok(remaining) => {
105 if !remaining.is_empty() {
106 cleanup_ok = false;
107 ctx.logger.warn(&format!(
108 "Ralph hooks still present after cleanup: {}",
109 remaining.join(", ")
110 ));
111 }
112 }
113 Err(err) => {
114 if err.kind() == std::io::ErrorKind::NotFound {
115 ctx.logger.warn(&format!(
116 "Skipping hook cleanup verification (repo not present on filesystem): {err}"
117 ));
118 } else {
119 cleanup_ok = false;
120 ctx.logger
121 .warn(&format!("Failed to verify hook cleanup: {err}"));
122 }
123 }
124 }
125
126 let summary = build_pipeline_summary(ctx.timer.elapsed_formatted(), ctx.config, final_state);
131 print_final_summary(ctx.colors, &summary, ctx.logger);
132
133 if ctx.config.features.checkpoint_enabled {
134 if let Err(err) = clear_checkpoint_with_workspace(ctx.workspace) {
135 ctx.logger
136 .warn(&format!("Failed to clear checkpoint: {err}"));
137 }
138 }
139
140 crate::files::cleanup_generated_files_with_workspace(ctx.workspace);
149 if !crate::git_helpers::try_remove_ralph_dir(repo_root) {
150 cleanup_ok = false;
151 let remaining = crate::git_helpers::verify_ralph_dir_removed(repo_root);
152 ctx.logger.warn(&format!(
153 "Ralph git dir still present after cleanup: {}",
154 remaining.join(", ")
155 ));
156 }
157
158 if cleanup_ok {
159 crate::git_helpers::clear_agent_phase_global_state();
164 agent_phase_guard.disarm();
165 } else {
166 ctx.logger.warn(
167 "Agent phase cleanup incomplete; leaving AgentPhaseGuard armed for Drop best-effort",
168 );
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::{finalize_pipeline, FinalizeContext};
175 use crate::config::Config;
176 use crate::git_helpers::{
177 agent_phase_test_lock, clear_agent_phase_global_state, get_agent_phase_paths_for_test,
178 set_agent_phase_paths_for_test, GitHelpers,
179 };
180 use crate::logger::{Colors, Logger};
181 use crate::pipeline::{AgentPhaseGuard, Timer};
182 use crate::reducer::state::PipelineState;
183 use crate::workspace::WorkspaceFs;
184
185 #[test]
186 fn test_summary_derives_from_reducer_metrics() {
187 let mut state = PipelineState::initial(5, 2);
188 state.metrics.dev_iterations_completed = 3;
189 state.metrics.review_runs_total = 4;
190 state.metrics.commits_created_total = 3;
191
192 let dev_runs_completed = state.metrics.dev_iterations_completed as usize;
194 let dev_runs_total = state.metrics.max_dev_iterations as usize;
195 let review_runs = state.metrics.review_runs_total as usize;
196 let changes_detected = state.metrics.commits_created_total as usize;
197
198 assert_eq!(dev_runs_completed, 3);
199 assert_eq!(dev_runs_total, 5);
200 assert_eq!(review_runs, 4);
201 assert_eq!(changes_detected, 3);
202 }
203
204 #[test]
205 fn test_metrics_reflect_actual_progress_not_config() {
206 let mut state = PipelineState::initial(10, 5);
207
208 state.metrics.dev_iterations_completed = 2;
210 state.metrics.review_runs_total = 0;
211
212 assert_eq!(state.metrics.dev_iterations_completed, 2);
214 assert_eq!(state.metrics.max_dev_iterations, 10);
215 }
216
217 #[test]
218 fn test_summary_no_drift_from_runtime_counters() {
219 let mut state = PipelineState::initial(10, 5);
220
221 state.metrics.dev_iterations_completed = 7;
223 state.metrics.review_runs_total = 3;
224 state.metrics.commits_created_total = 8;
225
226 let runtime_dev_completed = 5; let runtime_review_runs = 2; let dev_runs = state.metrics.dev_iterations_completed as usize;
232 let review_runs = state.metrics.review_runs_total as usize;
233 let commits = state.metrics.commits_created_total as usize;
234
235 assert_eq!(dev_runs, 7); assert_eq!(review_runs, 3); assert_eq!(commits, 8); assert_ne!(dev_runs, runtime_dev_completed);
241 assert_ne!(review_runs, runtime_review_runs);
242 }
243
244 #[test]
245 fn test_summary_uses_all_reducer_metrics() {
246 let mut state = PipelineState::initial(5, 3);
247
248 state.metrics.dev_iterations_started = 5;
250 state.metrics.dev_iterations_completed = 5;
251 state.metrics.dev_attempts_total = 7; state.metrics.analysis_attempts_total = 5;
253 state.metrics.review_passes_started = 3;
254 state.metrics.review_passes_completed = 3;
255 state.metrics.review_runs_total = 3;
256 state.metrics.fix_runs_total = 2;
257 state.metrics.commits_created_total = 6; state.metrics.xsd_retry_attempts_total = 2;
259 state.metrics.same_agent_retry_attempts_total = 1;
260
261 let dev_runs_completed = state.metrics.dev_iterations_completed as usize;
263 let dev_runs_total = state.metrics.max_dev_iterations as usize;
264 let review_passes_completed = state.metrics.review_passes_completed as usize;
265 let review_passes_total = state.metrics.max_review_passes as usize;
266 let review_runs_total = state.metrics.review_runs_total as usize;
267 let changes_detected = state.metrics.commits_created_total as usize;
268
269 assert_eq!(dev_runs_completed, 5);
271 assert_eq!(dev_runs_total, 5);
272 assert_eq!(review_passes_completed, 3);
273 assert_eq!(review_passes_total, 3);
274 assert_eq!(review_runs_total, 3);
275 assert_eq!(changes_detected, 6);
276
277 }
280
281 #[test]
282 fn test_partial_run_shows_actual_not_configured() {
283 let mut state = PipelineState::initial(10, 5);
284
285 state.metrics.dev_iterations_completed = 3;
287 state.metrics.review_passes_completed = 1;
288 state.metrics.commits_created_total = 3;
289
290 assert_eq!(state.metrics.dev_iterations_completed, 3);
291 assert_eq!(state.metrics.max_dev_iterations, 10);
292 assert_eq!(state.metrics.review_passes_completed, 1);
293 assert_eq!(state.metrics.max_review_passes, 5);
294 }
295
296 #[test]
297 fn test_generated_files_includes_all_artifacts() {
298 use crate::files::io::agent_files::GENERATED_FILES;
299 assert!(
305 GENERATED_FILES.contains(&".agent/PLAN.md"),
306 "GENERATED_FILES must include .agent/PLAN.md"
307 );
308 assert!(
309 GENERATED_FILES.contains(&".agent/commit-message.txt"),
310 "GENERATED_FILES must include .agent/commit-message.txt"
311 );
312 assert!(
313 GENERATED_FILES.contains(&".agent/checkpoint.json.tmp"),
314 "GENERATED_FILES must include .agent/checkpoint.json.tmp"
315 );
316 assert!(
317 GENERATED_FILES.contains(&".git/ralph/no_agent_commit"),
318 "GENERATED_FILES must include .git/ralph/no_agent_commit"
319 );
320 }
321
322 #[test]
323 fn test_finalize_pipeline_keeps_global_cleanup_state_when_guard_stays_armed() {
324 let _lock = agent_phase_test_lock().lock().unwrap();
325 let tempdir = tempfile::tempdir().unwrap();
326 let repo_root = tempdir.path();
327 let _repo = git2::Repository::init(repo_root).unwrap();
328
329 let ralph_dir = repo_root.join(".git/ralph");
330 std::fs::create_dir_all(&ralph_dir).unwrap();
331 std::fs::write(ralph_dir.join("quarantine.bin"), "keep").unwrap();
332 let hooks_dir = repo_root.join(".git/hooks");
333 std::fs::create_dir_all(&hooks_dir).unwrap();
334
335 set_agent_phase_paths_for_test(
336 Some(repo_root.to_path_buf()),
337 Some(ralph_dir.clone()),
338 Some(hooks_dir.clone()),
339 );
340
341 let workspace = WorkspaceFs::new(repo_root.to_path_buf());
342 let logger = Logger::new(Colors::with_enabled(false));
343 let config = Config::test_default();
344 let timer = Timer::new();
345 let final_state = PipelineState::initial(1, 1);
346 let mut helpers = GitHelpers::default();
347 let mut guard = AgentPhaseGuard::new(&mut helpers, &logger, &workspace);
348
349 finalize_pipeline(
350 &mut guard,
351 FinalizeContext {
352 logger: &logger,
353 colors: Colors::with_enabled(false),
354 config: &config,
355 timer: &timer,
356 workspace: &workspace,
357 },
358 &final_state,
359 None,
360 );
361
362 let actual = get_agent_phase_paths_for_test();
363 assert_eq!(
364 actual,
365 (
366 Some(repo_root.to_path_buf()),
367 Some(ralph_dir),
368 Some(hooks_dir),
369 ),
370 "finalize_pipeline must leave fallback cleanup paths intact when cleanup fails and the guard remains armed"
371 );
372
373 drop(guard);
374 clear_agent_phase_global_state();
375 }
376}