Skip to main content

oven_cli/pipeline/
state.rs

1use crate::db::RunStatus;
2
3impl RunStatus {
4    /// Determine the next state in the pipeline based on review findings and cycle count.
5    ///
6    /// - After implementing, always review.
7    /// - If reviewer finds issues and we haven't hit max cycles, fix.
8    /// - If reviewer finds issues at max cycles, move to awaiting merge (unresolved
9    ///   findings are posted on the PR for human review, not treated as failure).
10    /// - Clean review goes to awaiting merge (waiting for PR to be merged).
11    /// - After fixing, go back to reviewing.
12    /// - After awaiting merge, proceed to merging.
13    /// - After merging, complete.
14    #[must_use]
15    pub const fn next(self, has_findings: bool, cycle: u32) -> Self {
16        match self {
17            Self::Pending => Self::Implementing,
18            Self::Implementing | Self::Fixing => Self::Reviewing,
19            Self::Reviewing if has_findings && cycle < 2 => Self::Fixing,
20            Self::Reviewing => Self::AwaitingMerge,
21            Self::AwaitingMerge => Self::Merging,
22            Self::Merging => Self::Complete,
23            Self::Complete | Self::Failed => Self::Failed,
24        }
25    }
26
27    pub const fn is_terminal(self) -> bool {
28        matches!(self, Self::Complete | Self::Failed)
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use proptest::prelude::*;
35
36    use super::*;
37
38    const ALL_STATUSES: [RunStatus; 8] = [
39        RunStatus::Pending,
40        RunStatus::Implementing,
41        RunStatus::Reviewing,
42        RunStatus::Fixing,
43        RunStatus::AwaitingMerge,
44        RunStatus::Merging,
45        RunStatus::Complete,
46        RunStatus::Failed,
47    ];
48
49    proptest! {
50        #[test]
51        fn next_never_panics(idx in 0..8usize, has_findings: bool, cycle in 0..50u32) {
52            let status = ALL_STATUSES[idx];
53            // Should never panic regardless of inputs
54            let _ = status.next(has_findings, cycle);
55        }
56
57        #[test]
58        fn terminal_states_stay_terminal(has_findings: bool, cycle in 0..50u32) {
59            assert!(RunStatus::Complete.next(has_findings, cycle).is_terminal());
60            assert!(RunStatus::Failed.next(has_findings, cycle).is_terminal());
61        }
62
63        #[test]
64        fn reviewing_with_findings_past_max_awaits_merge(cycle in 2..50u32) {
65            assert_eq!(RunStatus::Reviewing.next(true, cycle), RunStatus::AwaitingMerge);
66        }
67
68        #[test]
69        fn reviewing_clean_always_awaits_merge(cycle in 0..50u32) {
70            assert_eq!(RunStatus::Reviewing.next(false, cycle), RunStatus::AwaitingMerge);
71        }
72    }
73
74    #[test]
75    fn pending_to_implementing() {
76        assert_eq!(RunStatus::Pending.next(false, 0), RunStatus::Implementing);
77    }
78
79    #[test]
80    fn implementing_to_reviewing() {
81        assert_eq!(RunStatus::Implementing.next(false, 0), RunStatus::Reviewing);
82    }
83
84    #[test]
85    fn clean_review_to_awaiting_merge() {
86        assert_eq!(RunStatus::Reviewing.next(false, 1), RunStatus::AwaitingMerge);
87    }
88
89    #[test]
90    fn awaiting_merge_to_merging() {
91        assert_eq!(RunStatus::AwaitingMerge.next(false, 0), RunStatus::Merging);
92    }
93
94    #[test]
95    fn findings_under_max_cycles_to_fixing() {
96        assert_eq!(RunStatus::Reviewing.next(true, 1), RunStatus::Fixing);
97    }
98
99    #[test]
100    fn findings_at_max_cycles_to_awaiting_merge() {
101        assert_eq!(RunStatus::Reviewing.next(true, 2), RunStatus::AwaitingMerge);
102    }
103
104    #[test]
105    fn fixing_back_to_reviewing() {
106        assert_eq!(RunStatus::Fixing.next(false, 1), RunStatus::Reviewing);
107    }
108
109    #[test]
110    fn merging_to_complete() {
111        assert_eq!(RunStatus::Merging.next(false, 0), RunStatus::Complete);
112    }
113
114    #[test]
115    fn terminal_states_go_to_failed() {
116        assert_eq!(RunStatus::Complete.next(false, 0), RunStatus::Failed);
117        assert_eq!(RunStatus::Failed.next(false, 0), RunStatus::Failed);
118    }
119
120    #[test]
121    fn is_terminal() {
122        assert!(RunStatus::Complete.is_terminal());
123        assert!(RunStatus::Failed.is_terminal());
124        assert!(!RunStatus::Pending.is_terminal());
125        assert!(!RunStatus::Implementing.is_terminal());
126        assert!(!RunStatus::Reviewing.is_terminal());
127        assert!(!RunStatus::Fixing.is_terminal());
128        assert!(!RunStatus::AwaitingMerge.is_terminal());
129        assert!(!RunStatus::Merging.is_terminal());
130    }
131}