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 =
107            CliArgsSnapshot::new(5, 2, "test".to_string(), None, false, true, 2, false, None);
108        let dev_config =
109            AgentConfigSnapshot::new("claude".into(), "cmd".into(), "-o".into(), None, true);
110        let rev_config =
111            AgentConfigSnapshot::new("codex".into(), "cmd".into(), "-o".into(), None, true);
112
113        let original_run_id = uuid::Uuid::new_v4().to_string();
114        let checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
115            phase: PipelinePhase::Development,
116            iteration: 2,
117            total_iterations: 5,
118            reviewer_pass: 0,
119            total_reviewer_passes: 2,
120            developer_agent: "claude",
121            reviewer_agent: "codex",
122            cli_args,
123            developer_agent_config: dev_config,
124            reviewer_agent_config: rev_config,
125            rebase_state: RebaseState::default(),
126            git_user_name: None,
127            git_user_email: None,
128            run_id: &original_run_id,
129            parent_run_id: None,
130            resume_count: 1,
131            actual_developer_runs: 2,
132            actual_reviewer_runs: 0,
133        });
134
135        let run_ctx = RunContext::from_checkpoint(&checkpoint);
136
137        assert_ne!(
138            run_ctx.run_id, original_run_id,
139            "new run_id should be generated"
140        );
141        assert_eq!(run_ctx.parent_run_id, Some(original_run_id));
142        assert_eq!(run_ctx.resume_count, 2, "resume_count should increment");
143        assert_eq!(run_ctx.actual_developer_runs, 2);
144        assert_eq!(run_ctx.actual_reviewer_runs, 0);
145    }
146
147    #[test]
148    fn test_run_context_with_developer_runs() {
149        let ctx = RunContext::new().with_developer_runs(5);
150        assert_eq!(ctx.actual_developer_runs, 5);
151    }
152
153    #[test]
154    fn test_run_context_with_reviewer_runs() {
155        let ctx = RunContext::new().with_reviewer_runs(3);
156        assert_eq!(ctx.actual_reviewer_runs, 3);
157    }
158}