edict/commands/protocol/
exit_policy.rs1use std::process::ExitCode;
16
17use super::render::{ProtocolGuidance, ProtocolStatus};
18use crate::commands::doctor::OutputFormat;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[repr(u8)]
26pub enum ProtocolExitCode {
27 Success = 0,
29 OperationalError = 1,
31 UsageError = 2,
33}
34
35impl From<ProtocolExitCode> for ExitCode {
36 fn from(code: ProtocolExitCode) -> ExitCode {
37 ExitCode::from(code as u8)
38 }
39}
40
41#[derive(Debug, thiserror::Error)]
47#[error("edict protocol: {context}: {detail}")]
48pub struct ProtocolExitError {
49 pub code: ProtocolExitCode,
50 pub context: String,
51 pub detail: String,
52}
53
54impl ProtocolExitError {
55 pub fn operational(context: impl Into<String>, detail: impl Into<String>) -> Self {
57 Self {
58 code: ProtocolExitCode::OperationalError,
59 context: context.into(),
60 detail: detail.into(),
61 }
62 }
63
64 pub fn into_exit_error(self) -> crate::error::ExitError {
66 crate::error::ExitError::new(self.code as u8, self.to_string())
67 }
68}
69
70#[allow(dead_code)]
75pub struct ProtocolResult {
76 pub exit_code: ProtocolExitCode,
77 pub guidance: Option<ProtocolGuidance>,
78}
79
80impl ProtocolResult {
81 #[allow(dead_code)]
83 pub fn success(guidance: ProtocolGuidance) -> Self {
84 Self {
85 exit_code: ProtocolExitCode::Success,
86 guidance: Some(guidance),
87 }
88 }
89
90 #[allow(dead_code)]
93 pub fn operational_error() -> Self {
94 Self {
95 exit_code: ProtocolExitCode::OperationalError,
96 guidance: None,
97 }
98 }
99}
100
101#[allow(dead_code)]
107pub fn exit_code_for_status(_status: ProtocolStatus) -> ProtocolExitCode {
108 ProtocolExitCode::Success
111}
112
113#[allow(dead_code)]
118pub fn write_stderr_diagnostic(context: &str, detail: &str) {
119 eprintln!("edict protocol: {context}: {detail}");
120}
121
122#[allow(dead_code)]
128pub fn render_and_exit(
129 guidance: &ProtocolGuidance,
130 format: OutputFormat,
131) -> anyhow::Result<ProtocolExitCode> {
132 let output = super::render::render(guidance, format)
133 .map_err(|e| anyhow::anyhow!("render error: {}", e))?;
134 println!("{}", output);
135 Ok(exit_code_for_status(guidance.status))
136}
137
138pub fn render_guidance(guidance: &ProtocolGuidance, format: OutputFormat) -> anyhow::Result<()> {
144 let output = super::render::render(guidance, format)
145 .map_err(|e| anyhow::anyhow!("render error: {}", e))?;
146 println!("{}", output);
147 Ok(())
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::commands::protocol::render::ProtocolGuidance;
154
155 #[test]
156 fn all_statuses_map_to_success() {
157 let statuses = vec![
158 ProtocolStatus::Ready,
159 ProtocolStatus::Blocked,
160 ProtocolStatus::Resumable,
161 ProtocolStatus::NeedsReview,
162 ProtocolStatus::HasResources,
163 ProtocolStatus::Clean,
164 ProtocolStatus::HasWork,
165 ProtocolStatus::Fresh,
166 ];
167 for status in statuses {
168 assert_eq!(
169 exit_code_for_status(status),
170 ProtocolExitCode::Success,
171 "status {:?} should map to Success",
172 status
173 );
174 }
175 }
176
177 #[test]
178 fn exit_code_values() {
179 assert_eq!(ProtocolExitCode::Success as u8, 0);
180 assert_eq!(ProtocolExitCode::OperationalError as u8, 1);
181 assert_eq!(ProtocolExitCode::UsageError as u8, 2);
182 }
183
184 #[test]
185 fn exit_code_to_std_exit_code() {
186 let code: ExitCode = ProtocolExitCode::Success.into();
187 let _ = code;
189
190 let code: ExitCode = ProtocolExitCode::OperationalError.into();
191 let _ = code;
192
193 let code: ExitCode = ProtocolExitCode::UsageError.into();
194 let _ = code;
195 }
196
197 #[test]
198 fn protocol_result_success() {
199 let guidance = ProtocolGuidance::new("start");
200 let result = ProtocolResult::success(guidance);
201 assert_eq!(result.exit_code, ProtocolExitCode::Success);
202 assert!(result.guidance.is_some());
203 }
204
205 #[test]
206 fn protocol_result_operational_error() {
207 let result = ProtocolResult::operational_error();
208 assert_eq!(result.exit_code, ProtocolExitCode::OperationalError);
209 assert!(result.guidance.is_none());
210 }
211
212 #[test]
213 fn blocked_status_still_exits_zero() {
214 let mut guidance = ProtocolGuidance::new("start");
215 guidance.blocked("bone claimed by another agent".to_string());
216 assert_eq!(
217 exit_code_for_status(guidance.status),
218 ProtocolExitCode::Success
219 );
220 }
221
222 #[test]
223 fn needs_review_status_still_exits_zero() {
224 let mut guidance = ProtocolGuidance::new("review");
225 guidance.status = ProtocolStatus::NeedsReview;
226 assert_eq!(
227 exit_code_for_status(guidance.status),
228 ProtocolExitCode::Success
229 );
230 }
231
232 #[test]
233 fn has_resources_status_still_exits_zero() {
234 let mut guidance = ProtocolGuidance::new("cleanup");
235 guidance.status = ProtocolStatus::HasResources;
236 assert_eq!(
237 exit_code_for_status(guidance.status),
238 ProtocolExitCode::Success
239 );
240 }
241
242 #[test]
243 fn protocol_exit_error_operational() {
244 let err = ProtocolExitError::operational("start", "config not found");
245 assert_eq!(err.code, ProtocolExitCode::OperationalError);
246 assert_eq!(err.context, "start");
247 assert_eq!(err.detail, "config not found");
248 let msg = err.to_string();
249 assert!(msg.contains("edict protocol: start: config not found"));
250 }
251
252 #[test]
253 fn protocol_exit_error_to_exit_error() {
254 let err = ProtocolExitError::operational("cleanup", "bus not available");
255 let exit_err = err.into_exit_error();
256 assert_eq!(exit_err.exit_code(), ExitCode::from(1u8));
258 }
259
260 #[test]
261 fn operational_error_exit_code_is_one() {
262 let err = ProtocolExitError::operational("start", "tool missing");
263 assert_eq!(err.code as u8, 1);
264 }
265
266 #[test]
267 fn stderr_diagnostic_format() {
268 write_stderr_diagnostic("start", "config not found");
271 }
272}