use crate::stage::Stage;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Auto,
Supervise,
}
impl Mode {
pub fn should_gate(self, stage: Stage, consecutive_failures: u32) -> bool {
match stage {
Stage::Ship => true,
Stage::Validate => match self {
Mode::Supervise => true,
Mode::Auto => consecutive_failures >= MAX_CONSECUTIVE_FAILURES,
},
_ => false,
}
}
pub fn should_auto_loop(self, stage: Stage) -> bool {
matches!(stage, Stage::Validate) && matches!(self, Mode::Auto)
}
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Mode::Auto => "auto",
Mode::Supervise => "supervise",
};
f.write_str(name)
}
}
impl FromStr for Mode {
type Err = ModeParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"auto" => Ok(Mode::Auto),
"supervise" | "supervised" => Ok(Mode::Supervise),
other => Err(ModeParseError(other.to_string())),
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("unsupported mode `{0}`; expected auto or supervise")]
pub struct ModeParseError(String);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str_accepts_canonical_and_alias() {
assert_eq!("auto".parse::<Mode>().unwrap(), Mode::Auto);
assert_eq!("AUTO".parse::<Mode>().unwrap(), Mode::Auto);
assert_eq!("supervise".parse::<Mode>().unwrap(), Mode::Supervise);
assert_eq!("supervised".parse::<Mode>().unwrap(), Mode::Supervise);
}
#[test]
fn from_str_rejects_unknown() {
let err = "yolo".parse::<Mode>().unwrap_err();
assert!(err.to_string().contains("yolo"));
}
#[test]
fn auto_does_not_gate_validate_until_failure_threshold() {
assert!(!Mode::Auto.should_gate(Stage::Validate, 0));
assert!(!Mode::Auto.should_gate(Stage::Validate, 2));
assert!(Mode::Auto.should_gate(Stage::Validate, MAX_CONSECUTIVE_FAILURES));
assert!(Mode::Auto.should_gate(Stage::Validate, 9));
}
#[test]
fn supervise_always_gates_validate() {
assert!(Mode::Supervise.should_gate(Stage::Validate, 0));
assert!(Mode::Supervise.should_gate(Stage::Validate, 5));
}
#[test]
fn ship_always_gates_in_both_modes() {
assert!(Mode::Auto.should_gate(Stage::Ship, 0));
assert!(Mode::Supervise.should_gate(Stage::Ship, 0));
}
#[test]
fn non_gate_stages_never_gate() {
for stage in [Stage::Define, Stage::Plan, Stage::Code] {
assert!(!Mode::Auto.should_gate(stage, 99));
assert!(!Mode::Supervise.should_gate(stage, 99));
}
}
#[test]
fn auto_loops_validate_supervise_does_not() {
assert!(Mode::Auto.should_auto_loop(Stage::Validate));
assert!(!Mode::Supervise.should_auto_loop(Stage::Validate));
assert!(!Mode::Auto.should_auto_loop(Stage::Code));
}
#[test]
fn display_round_trips_through_from_str() {
for mode in [Mode::Auto, Mode::Supervise] {
assert_eq!(mode.to_string().parse::<Mode>().unwrap(), mode);
}
}
}