remem/runtime_config/
promotion.rs1use anyhow::{bail, Result};
2use toml_edit::{DocumentMut, Item};
3
4const DEFAULT_SUMMARY_GATE_MODE: &str = "enforce";
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum SummaryGateMode {
8 Off,
9 Shadow,
10 Enforce,
11}
12
13pub fn summary_gate_mode() -> Result<SummaryGateMode> {
14 let mut doc = super::read_config_doc_or_default()?;
15 ensure_defaults(&mut doc)?;
16 let mode = doc
17 .get("promotion")
18 .and_then(Item::as_table)
19 .and_then(|table| super::optional_str(table, "summary_gate_mode"))
20 .unwrap_or_else(|| DEFAULT_SUMMARY_GATE_MODE.to_string());
21 parse_summary_gate_mode(&mode)
22}
23
24pub(super) fn ensure_defaults(doc: &mut DocumentMut) -> Result<()> {
25 let promotion = super::top_table_mut(doc, "promotion")?;
26 super::set_str_if_missing(promotion, "summary_gate_mode", DEFAULT_SUMMARY_GATE_MODE);
27 Ok(())
28}
29
30fn parse_summary_gate_mode(raw: &str) -> Result<SummaryGateMode> {
31 match raw.trim().to_ascii_lowercase().as_str() {
32 "off" => Ok(SummaryGateMode::Off),
33 "shadow" => Ok(SummaryGateMode::Shadow),
34 "enforce" => Ok(SummaryGateMode::Enforce),
35 other => {
36 bail!("unknown promotion.summary_gate_mode: {other}; expected off, shadow, or enforce")
37 }
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 fn with_promotion_config_path<T>(path: &std::path::Path, f: impl FnOnce() -> T) -> T {
46 let _guard = super::super::TEST_ENV_LOCK
47 .lock()
48 .expect("env lock should acquire");
49 let old = std::env::var("REMEM_CONFIG").ok();
50 unsafe { std::env::set_var("REMEM_CONFIG", path) };
51 let result = f();
52 match old {
53 Some(value) => unsafe { std::env::set_var("REMEM_CONFIG", value) },
54 None => unsafe { std::env::remove_var("REMEM_CONFIG") },
55 }
56 result
57 }
58
59 fn promotion_config_path(label: &str) -> std::path::PathBuf {
60 std::env::temp_dir().join(format!(
61 "remem-{label}-{}-{}.toml",
62 std::process::id(),
63 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
64 ))
65 }
66
67 #[test]
68 fn default_config_enables_summary_gate_enforce_mode() {
69 let text = super::super::default_config_text();
70 assert!(text.contains("summary_gate_mode = \"enforce\""), "{text}");
71 }
72
73 #[test]
74 fn summary_gate_mode_reads_config_value() -> Result<()> {
75 let path = promotion_config_path("summary-gate-mode");
76 with_promotion_config_path(&path, || -> Result<()> {
77 super::super::init_config()?;
78 super::super::set_config_value("promotion.summary_gate_mode", "shadow")?;
79 assert_eq!(summary_gate_mode()?, SummaryGateMode::Shadow);
80
81 super::super::set_config_value("promotion.summary_gate_mode", "off")?;
82 assert_eq!(summary_gate_mode()?, SummaryGateMode::Off);
83 Ok(())
84 })?;
85 std::fs::remove_file(path)?;
86 Ok(())
87 }
88
89 #[test]
90 fn summary_gate_mode_rejects_unknown_value() -> Result<()> {
91 let path = promotion_config_path("summary-gate-mode-invalid");
92 with_promotion_config_path(&path, || -> Result<()> {
93 std::fs::write(&path, "[promotion]\nsummary_gate_mode = \"maybe\"\n")?;
94 let err = summary_gate_mode().expect_err("invalid mode must fail closed");
95 assert!(
96 err.to_string()
97 .contains("unknown promotion.summary_gate_mode"),
98 "{err}"
99 );
100 Ok(())
101 })?;
102 std::fs::remove_file(path)?;
103 Ok(())
104 }
105}