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