Skip to main content

ralph_workflow/app/
finalization.rs

1//! Pipeline finalization and cleanup.
2//!
3//! This module handles the final phase of the pipeline including cleanup,
4//! final summary, and checkpoint clearing.
5//!
6//! Note: PROMPT.md permission restoration is now handled by the reducer's
7//! `Effect::RestorePromptPermissions` during the `Finalizing` phase, ensuring
8//! it goes through the effect system for proper testability.
9
10use 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/// Context for pipeline finalization.
22pub struct FinalizeContext<'a> {
23    pub logger: &'a Logger,
24    pub colors: Colors,
25    pub config: &'a Config,
26    pub timer: &'a Timer,
27    pub workspace: &'a dyn Workspace,
28}
29
30/// Finalizes the pipeline: cleans up and prints summary.
31///
32/// Commits now happen per-iteration during development and per-cycle during review,
33/// so this function only handles cleanup and final summary.
34///
35/// # Arguments
36///
37/// * `ctx` - Finalization context with logger, config, timer, and workspace
38/// * `final_state` - Final pipeline state from reducer (source of truth for metrics)
39pub fn finalize_pipeline(
40    agent_phase_guard: &mut AgentPhaseGuard,
41    ctx: FinalizeContext<'_>,
42    final_state: &PipelineState,
43    prompt_monitor: Option<PromptMonitor>,
44) {
45    // Stop the PROMPT.md monitor if it was started
46    if let Some(monitor) = prompt_monitor {
47        monitor.stop();
48    }
49
50    // End agent phase and clean up
51    crate::git_helpers::end_agent_phase();
52    crate::git_helpers::disable_git_wrapper(agent_phase_guard.git_helpers);
53    if let Err(err) = crate::git_helpers::uninstall_hooks(ctx.logger) {
54        ctx.logger
55            .warn(&format!("Failed to uninstall Ralph hooks: {err}"));
56    }
57
58    // Note: Individual commits were created per-iteration during development
59    // and per-cycle during review. The final commit phase has been removed.
60
61    // Final summary derived exclusively from reducer state
62    let summary = PipelineSummary {
63        total_time: ctx.timer.elapsed_formatted(),
64        dev_runs_completed: final_state.metrics.dev_iterations_completed as usize,
65        dev_runs_total: final_state.metrics.max_dev_iterations as usize,
66        review_passes_completed: final_state.metrics.review_passes_completed as usize,
67        review_passes_total: final_state.metrics.max_review_passes as usize,
68        review_runs: final_state.metrics.review_runs_total as usize,
69        changes_detected: final_state.metrics.commits_created_total as usize,
70        isolation_mode: ctx.config.isolation_mode,
71        verbose: ctx.config.verbosity.is_verbose(),
72        review_summary: None,
73    };
74    print_final_summary(ctx.colors, &summary, ctx.logger);
75
76    if ctx.config.features.checkpoint_enabled {
77        if let Err(err) = clear_checkpoint_with_workspace(ctx.workspace) {
78            ctx.logger
79                .warn(&format!("Failed to clear checkpoint: {err}"));
80        }
81    }
82
83    // Note: PROMPT.md write permissions are now restored via the reducer's
84    // Effect::RestorePromptPermissions during the Finalizing phase.
85    // This ensures the operation goes through the effect system for testability.
86
87    agent_phase_guard.disarm();
88}
89
90#[cfg(test)]
91mod tests {
92    use crate::reducer::state::PipelineState;
93
94    #[test]
95    fn test_summary_derives_from_reducer_metrics() {
96        let mut state = PipelineState::initial(5, 2);
97        state.metrics.dev_iterations_completed = 3;
98        state.metrics.review_runs_total = 4;
99        state.metrics.commits_created_total = 3;
100
101        // Summary should use reducer metrics, not runtime counters
102        let dev_runs_completed = state.metrics.dev_iterations_completed as usize;
103        let dev_runs_total = state.metrics.max_dev_iterations as usize;
104        let review_runs = state.metrics.review_runs_total as usize;
105        let changes_detected = state.metrics.commits_created_total as usize;
106
107        assert_eq!(dev_runs_completed, 3);
108        assert_eq!(dev_runs_total, 5);
109        assert_eq!(review_runs, 4);
110        assert_eq!(changes_detected, 3);
111    }
112
113    #[test]
114    fn test_metrics_reflect_actual_progress_not_config() {
115        let mut state = PipelineState::initial(10, 5);
116
117        // Simulate partial run: only 2 iterations completed out of 10 configured
118        state.metrics.dev_iterations_completed = 2;
119        state.metrics.review_runs_total = 0;
120
121        // Summary should show actual progress (2), not config (10)
122        assert_eq!(state.metrics.dev_iterations_completed, 2);
123        assert_eq!(state.metrics.max_dev_iterations, 10);
124    }
125
126    #[test]
127    fn test_summary_no_drift_from_runtime_counters() {
128        let mut state = PipelineState::initial(10, 5);
129
130        // Simulate reducer metrics
131        state.metrics.dev_iterations_completed = 7;
132        state.metrics.review_runs_total = 3;
133        state.metrics.commits_created_total = 8;
134
135        // Simulate hypothetical runtime counters (these should NOT be used)
136        let runtime_dev_completed = 5; // WRONG VALUE - should be ignored
137        let runtime_review_runs = 2; // WRONG VALUE - should be ignored
138
139        // Summary must use reducer metrics, not runtime counters
140        let dev_runs = state.metrics.dev_iterations_completed as usize;
141        let review_runs = state.metrics.review_runs_total as usize;
142        let commits = state.metrics.commits_created_total as usize;
143
144        assert_eq!(dev_runs, 7); // From reducer, not runtime
145        assert_eq!(review_runs, 3); // From reducer, not runtime
146        assert_eq!(commits, 8); // From reducer, not runtime
147
148        // Prove we're not using the wrong values
149        assert_ne!(dev_runs, runtime_dev_completed);
150        assert_ne!(review_runs, runtime_review_runs);
151    }
152
153    #[test]
154    fn test_summary_uses_all_reducer_metrics() {
155        let mut state = PipelineState::initial(5, 3);
156
157        // Simulate complete run metrics
158        state.metrics.dev_iterations_started = 5;
159        state.metrics.dev_iterations_completed = 5;
160        state.metrics.dev_attempts_total = 7; // Including continuations
161        state.metrics.analysis_attempts_total = 5;
162        state.metrics.review_passes_started = 3;
163        state.metrics.review_passes_completed = 3;
164        state.metrics.review_runs_total = 3;
165        state.metrics.fix_runs_total = 2;
166        state.metrics.commits_created_total = 6; // 5 dev + 1 final
167        state.metrics.xsd_retry_attempts_total = 2;
168        state.metrics.same_agent_retry_attempts_total = 1;
169
170        // Construct summary as finalize_pipeline does
171        let dev_runs_completed = state.metrics.dev_iterations_completed as usize;
172        let dev_runs_total = state.metrics.max_dev_iterations as usize;
173        let review_passes_completed = state.metrics.review_passes_completed as usize;
174        let review_passes_total = state.metrics.max_review_passes as usize;
175        let review_runs_total = state.metrics.review_runs_total as usize;
176        let changes_detected = state.metrics.commits_created_total as usize;
177
178        // Verify all values come from reducer metrics
179        assert_eq!(dev_runs_completed, 5);
180        assert_eq!(dev_runs_total, 5);
181        assert_eq!(review_passes_completed, 3);
182        assert_eq!(review_passes_total, 3);
183        assert_eq!(review_runs_total, 3);
184        assert_eq!(changes_detected, 6);
185
186        // Verify we're not using any separate runtime counters
187        // (this test proves the summary construction pattern)
188    }
189
190    #[test]
191    fn test_partial_run_shows_actual_not_configured() {
192        let mut state = PipelineState::initial(10, 5);
193
194        // Only partial progress
195        state.metrics.dev_iterations_completed = 3;
196        state.metrics.review_passes_completed = 1;
197        state.metrics.commits_created_total = 3;
198
199        assert_eq!(state.metrics.dev_iterations_completed, 3);
200        assert_eq!(state.metrics.max_dev_iterations, 10);
201        assert_eq!(state.metrics.review_passes_completed, 1);
202        assert_eq!(state.metrics.max_review_passes, 5);
203    }
204}