orchestrator_config/config/
invariant.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5pub struct InvariantConfig {
6 pub name: String,
8 #[serde(default)]
10 pub description: String,
11 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub command: Option<String>,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub expect_exit: Option<i32>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub capture_as: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub assert_expr: Option<String>,
23 #[serde(default)]
25 pub immutable: bool,
26 #[serde(default = "default_check_at")]
28 pub check_at: Vec<InvariantCheckPoint>,
29 #[serde(default)]
31 pub on_violation: OnViolation,
32 #[serde(default)]
34 pub protected_files: Vec<String>,
35}
36
37fn default_check_at() -> Vec<InvariantCheckPoint> {
38 vec![
39 InvariantCheckPoint::AfterImplement,
40 InvariantCheckPoint::BeforeComplete,
41 ]
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
46#[serde(rename_all = "snake_case")]
47pub enum InvariantCheckPoint {
48 BeforeCycle,
50 AfterImplement,
52 BeforeRestart,
54 BeforeComplete,
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum OnViolation {
62 #[default]
64 Halt,
65 Rollback,
67 Warn,
69}
70
71#[derive(Debug, Clone)]
73pub struct InvariantResult {
74 pub name: String,
76 pub passed: bool,
78 pub message: String,
80 pub on_violation: OnViolation,
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_invariant_config_defaults() {
90 let json = r#"{"name": "test", "description": "a test"}"#;
91 let cfg: InvariantConfig =
92 serde_json::from_str(json).expect("deserialize invariant config");
93 assert_eq!(cfg.name, "test");
94 assert!(!cfg.immutable);
95 assert_eq!(cfg.check_at.len(), 2);
96 assert!(cfg.check_at.contains(&InvariantCheckPoint::AfterImplement));
97 assert!(cfg.check_at.contains(&InvariantCheckPoint::BeforeComplete));
98 assert_eq!(cfg.on_violation, OnViolation::Halt);
99 assert!(cfg.protected_files.is_empty());
100 }
101
102 #[test]
103 fn test_invariant_config_full() {
104 let json = r#"{
105 "name": "no_unsafe",
106 "description": "No unsafe code",
107 "command": "grep -r unsafe src/",
108 "expect_exit": 1,
109 "assert_expr": "exit_code == 1",
110 "immutable": true,
111 "check_at": ["before_cycle", "after_implement"],
112 "on_violation": "rollback",
113 "protected_files": ["Cargo.toml", "src/main.rs"]
114 }"#;
115 let cfg: InvariantConfig =
116 serde_json::from_str(json).expect("deserialize full invariant config");
117 assert!(cfg.immutable);
118 assert_eq!(cfg.on_violation, OnViolation::Rollback);
119 assert_eq!(cfg.protected_files.len(), 2);
120 assert_eq!(cfg.check_at.len(), 2);
121 }
122
123 #[test]
124 fn test_on_violation_default() {
125 let v = OnViolation::default();
126 assert_eq!(v, OnViolation::Halt);
127 }
128
129 #[test]
130 fn test_checkpoint_serde_round_trip() {
131 for s in &[
132 "\"before_cycle\"",
133 "\"after_implement\"",
134 "\"before_restart\"",
135 "\"before_complete\"",
136 ] {
137 let cp: InvariantCheckPoint = serde_json::from_str(s).expect("deserialize checkpoint");
138 let json = serde_json::to_string(&cp).expect("serialize checkpoint");
139 assert_eq!(&json, s);
140 }
141 }
142
143 #[test]
144 fn test_on_violation_serde_round_trip() {
145 for s in &["\"halt\"", "\"rollback\"", "\"warn\""] {
146 let ov: OnViolation = serde_json::from_str(s).expect("deserialize on_violation");
147 let json = serde_json::to_string(&ov).expect("serialize on_violation");
148 assert_eq!(&json, s);
149 }
150 }
151}