1use crate::stage::Stage;
13use serde::{Deserialize, Serialize};
14use std::fmt;
15use std::str::FromStr;
16
17pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum Mode {
24 Auto,
26 Supervise,
28}
29
30impl Mode {
31 pub fn should_gate(self, stage: Stage, consecutive_failures: u32) -> bool {
38 match stage {
39 Stage::Ship => true,
40 Stage::Validate => match self {
41 Mode::Supervise => true,
42 Mode::Auto => consecutive_failures >= MAX_CONSECUTIVE_FAILURES,
43 },
44 _ => false,
45 }
46 }
47
48 pub fn should_auto_loop(self, stage: Stage) -> bool {
51 matches!(stage, Stage::Validate) && matches!(self, Mode::Auto)
52 }
53}
54
55impl fmt::Display for Mode {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 let name = match self {
58 Mode::Auto => "auto",
59 Mode::Supervise => "supervise",
60 };
61 f.write_str(name)
62 }
63}
64
65impl FromStr for Mode {
66 type Err = ModeParseError;
67
68 fn from_str(value: &str) -> Result<Self, Self::Err> {
69 match value.to_ascii_lowercase().as_str() {
70 "auto" => Ok(Mode::Auto),
71 "supervise" | "supervised" => Ok(Mode::Supervise),
72 other => Err(ModeParseError(other.to_string())),
73 }
74 }
75}
76
77#[derive(Debug, Clone, thiserror::Error)]
79#[error("unsupported mode `{0}`; expected auto or supervise")]
80pub struct ModeParseError(String);
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn from_str_accepts_canonical_and_alias() {
88 assert_eq!("auto".parse::<Mode>().unwrap(), Mode::Auto);
89 assert_eq!("AUTO".parse::<Mode>().unwrap(), Mode::Auto);
90 assert_eq!("supervise".parse::<Mode>().unwrap(), Mode::Supervise);
91 assert_eq!("supervised".parse::<Mode>().unwrap(), Mode::Supervise);
92 }
93
94 #[test]
95 fn from_str_rejects_unknown() {
96 let err = "yolo".parse::<Mode>().unwrap_err();
97 assert!(err.to_string().contains("yolo"));
98 }
99
100 #[test]
101 fn auto_does_not_gate_validate_until_failure_threshold() {
102 assert!(!Mode::Auto.should_gate(Stage::Validate, 0));
103 assert!(!Mode::Auto.should_gate(Stage::Validate, 2));
104 assert!(Mode::Auto.should_gate(Stage::Validate, MAX_CONSECUTIVE_FAILURES));
105 assert!(Mode::Auto.should_gate(Stage::Validate, 9));
106 }
107
108 #[test]
109 fn supervise_always_gates_validate() {
110 assert!(Mode::Supervise.should_gate(Stage::Validate, 0));
111 assert!(Mode::Supervise.should_gate(Stage::Validate, 5));
112 }
113
114 #[test]
115 fn ship_always_gates_in_both_modes() {
116 assert!(Mode::Auto.should_gate(Stage::Ship, 0));
117 assert!(Mode::Supervise.should_gate(Stage::Ship, 0));
118 }
119
120 #[test]
121 fn non_gate_stages_never_gate() {
122 for stage in [Stage::Define, Stage::Plan, Stage::Code] {
123 assert!(!Mode::Auto.should_gate(stage, 99));
124 assert!(!Mode::Supervise.should_gate(stage, 99));
125 }
126 }
127
128 #[test]
129 fn auto_loops_validate_supervise_does_not() {
130 assert!(Mode::Auto.should_auto_loop(Stage::Validate));
131 assert!(!Mode::Supervise.should_auto_loop(Stage::Validate));
132 assert!(!Mode::Auto.should_auto_loop(Stage::Code));
133 }
134
135 #[test]
136 fn display_round_trips_through_from_str() {
137 for mode in [Mode::Auto, Mode::Supervise] {
138 assert_eq!(mode.to_string().parse::<Mode>().unwrap(), mode);
139 }
140 }
141}