Skip to main content

ralph_workflow/checkpoint/
run_context.rs

1//! Run context for tracking pipeline execution lineage and state.
2//!
3//! This module provides the `RunContext` struct which tracks information
4//! about a pipeline run including its unique identifier, parent run (if resumed),
5//! and actual execution counts (separate from configured counts).
6
7use serde::{Deserialize, Serialize};
8
9/// Context for tracking pipeline execution lineage and state.
10///
11/// This tracks information about the current pipeline run that is separate
12/// from the configured parameters. It enables:
13/// - Unique identification of each run (run_id)
14/// - Tracking resume lineage (parent_run_id)
15/// - Counting how many times a session has been resumed (resume_count)
16/// - Tracking actual completed iterations vs configured iterations
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct RunContext {
19    /// Unique identifier for this run (UUID v4)
20    pub run_id: String,
21    /// Parent run ID if this is a resumed session
22    pub parent_run_id: Option<String>,
23    /// Number of times this session has been resumed
24    pub resume_count: u32,
25    /// Actual number of developer iterations that have completed
26    pub actual_developer_runs: u32,
27    /// Actual number of reviewer passes that have completed
28    pub actual_reviewer_runs: u32,
29}
30
31impl RunContext {
32    /// Create a new RunContext for a fresh run.
33    pub fn new() -> Self {
34        Self {
35            run_id: uuid::Uuid::new_v4().to_string(),
36            parent_run_id: None,
37            resume_count: 0,
38            actual_developer_runs: 0,
39            actual_reviewer_runs: 0,
40        }
41    }
42
43    /// Create a RunContext from a checkpoint (for resume scenarios).
44    pub fn from_checkpoint(checkpoint: &super::PipelineCheckpoint) -> Self {
45        Self {
46            run_id: uuid::Uuid::new_v4().to_string(), // New run_id for resumed run
47            parent_run_id: Some(checkpoint.run_id.clone()),
48            resume_count: checkpoint.resume_count + 1,
49            actual_developer_runs: checkpoint.actual_developer_runs,
50            actual_reviewer_runs: checkpoint.actual_reviewer_runs,
51        }
52    }
53
54    /// Update the actual developer runs count.
55    #[cfg(test)]
56    pub fn with_developer_runs(mut self, runs: u32) -> Self {
57        self.actual_developer_runs = runs;
58        self
59    }
60
61    /// Update the actual reviewer runs count.
62    #[cfg(test)]
63    pub fn with_reviewer_runs(mut self, runs: u32) -> Self {
64        self.actual_reviewer_runs = runs;
65        self
66    }
67
68    /// Record a completed developer iteration.
69    pub fn record_developer_iteration(&mut self) {
70        self.actual_developer_runs += 1;
71    }
72
73    /// Record a completed reviewer pass.
74    pub fn record_reviewer_pass(&mut self) {
75        self.actual_reviewer_runs += 1;
76    }
77}
78
79impl Default for RunContext {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::checkpoint::state::{
89        AgentConfigSnapshot, CheckpointParams, CliArgsSnapshot, PipelineCheckpoint, PipelinePhase,
90        RebaseState,
91    };
92
93    #[test]
94    fn test_run_context_new() {
95        let ctx = RunContext::new();
96        assert!(!ctx.run_id.is_empty());
97        assert!(ctx.parent_run_id.is_none());
98        assert_eq!(ctx.resume_count, 0);
99        assert_eq!(ctx.actual_developer_runs, 0);
100        assert_eq!(ctx.actual_reviewer_runs, 0);
101    }
102
103    #[test]
104    fn test_run_context_from_checkpoint() {
105        // Create a mock checkpoint
106        let cli_args = CliArgsSnapshot::new(5, 2, None, false, true, 2, false, None);
107        let dev_config =
108            AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
109        let rev_config =
110            AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
111
112        let original_run_id = uuid::Uuid::new_v4().to_string();
113        let checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
114            phase: PipelinePhase::Development,
115            iteration: 2,
116            total_iterations: 5,
117            reviewer_pass: 0,
118            total_reviewer_passes: 2,
119            developer_agent: "claude",
120            reviewer_agent: "codex",
121            cli_args,
122            developer_agent_config: dev_config,
123            reviewer_agent_config: rev_config,
124            rebase_state: RebaseState::default(),
125            git_user_name: None,
126            git_user_email: None,
127            run_id: &original_run_id,
128            parent_run_id: None,
129            resume_count: 1,
130            actual_developer_runs: 2,
131            actual_reviewer_runs: 0,
132        });
133
134        let run_ctx = RunContext::from_checkpoint(&checkpoint);
135
136        assert_ne!(
137            run_ctx.run_id, original_run_id,
138            "new run_id should be generated"
139        );
140        assert_eq!(run_ctx.parent_run_id, Some(original_run_id));
141        assert_eq!(run_ctx.resume_count, 2, "resume_count should increment");
142        assert_eq!(run_ctx.actual_developer_runs, 2);
143        assert_eq!(run_ctx.actual_reviewer_runs, 0);
144    }
145
146    #[test]
147    fn test_run_context_with_developer_runs() {
148        let ctx = RunContext::new().with_developer_runs(5);
149        assert_eq!(ctx.actual_developer_runs, 5);
150    }
151
152    #[test]
153    fn test_run_context_with_reviewer_runs() {
154        let ctx = RunContext::new().with_reviewer_runs(3);
155        assert_eq!(ctx.actual_reviewer_runs, 3);
156    }
157}