1#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
3pub enum AionSurfaceError {
4 #[error("dispatch failed for workflow '{workflow_id}' on channel '{channel_name}': {message}")]
6 DispatchFailed {
7 channel_name: String,
9 workflow_id: String,
11 message: String,
13 },
14
15 #[error(
17 "worker '{worker_id}' crashed while serving workflow '{workflow_id}' on channel '{channel_name}': {message}"
18 )]
19 WorkerCrashed {
20 channel_name: String,
22 workflow_id: String,
24 worker_id: String,
26 message: String,
28 },
29
30 #[error(
32 "signal delivery failed for signal '{signal_name}' to workflow '{workflow_id}' on channel '{channel_name}': {message}"
33 )]
34 SignalDeliveryFailed {
35 channel_name: String,
37 workflow_id: String,
39 signal_name: String,
41 message: String,
43 },
44
45 #[error(
47 "signal validation failed for signal '{signal_name}' to workflow '{workflow_id}' on channel '{channel_name}': {message}"
48 )]
49 SignalValidationFailed {
50 channel_name: String,
52 workflow_id: String,
54 signal_name: String,
56 message: String,
58 },
59
60 #[error(
62 "history streaming failed for workflow '{workflow_id}' on channel '{channel_name}': {message}"
63 )]
64 StreamingFailed {
65 channel_name: String,
67 workflow_id: String,
69 message: String,
71 },
72
73 #[error("invalid channel name input '{input}' for {part}: {message}")]
75 InvalidChannelName {
76 part: String,
78 input: String,
80 message: String,
82 },
83
84 #[error("channel lifecycle error for channel '{channel_name}': {message}")]
86 ChannelLifecycleError {
87 channel_name: String,
89 message: String,
91 },
92}
93
94#[cfg(test)]
95mod tests {
96 use std::error::Error;
97
98 use super::super::{dispatch_channel, history_channel, signal_channel};
99 use super::AionSurfaceError;
100
101 fn assert_error_trait<E: Error>(_: &E) {}
102
103 #[test]
104 fn implements_std_error_and_debug() -> Result<(), AionSurfaceError> {
105 let channel_name = String::from(history_channel("prod", "wf-123")?);
106 let error = AionSurfaceError::ChannelLifecycleError {
107 channel_name,
108 message: "teardown failed".to_owned(),
109 };
110 let debug_output = format!("{error:?}");
111
112 assert_error_trait(&error);
113 assert!(error.source().is_none());
114 assert!(!debug_output.is_empty());
115
116 Ok(())
117 }
118
119 #[test]
120 fn dispatch_display_includes_channel_and_workflow_context() -> Result<(), AionSurfaceError> {
121 let channel_name = String::from(dispatch_channel("prod", "email-queue")?);
122 let error = AionSurfaceError::DispatchFailed {
123 channel_name: channel_name.clone(),
124 workflow_id: "wf-123".to_owned(),
125 message: "conversation failed".to_owned(),
126 };
127 let display = error.to_string();
128
129 assert!(display.contains(&channel_name));
130 assert!(display.contains("wf-123"));
131 assert!(display.contains("conversation failed"));
132
133 Ok(())
134 }
135
136 #[test]
137 fn worker_crash_display_includes_channel_workflow_and_worker_context()
138 -> Result<(), AionSurfaceError> {
139 let channel_name = String::from(dispatch_channel("prod", "email-queue")?);
140 let error = AionSurfaceError::WorkerCrashed {
141 channel_name: channel_name.clone(),
142 workflow_id: "wf-123".to_owned(),
143 worker_id: "worker-9".to_owned(),
144 message: "linked process exited".to_owned(),
145 };
146 let display = error.to_string();
147
148 assert!(display.contains(&channel_name));
149 assert!(display.contains("wf-123"));
150 assert!(display.contains("worker-9"));
151 assert!(display.contains("linked process exited"));
152
153 Ok(())
154 }
155
156 #[test]
157 fn signal_display_includes_channel_workflow_and_signal_context() -> Result<(), AionSurfaceError>
158 {
159 let channel_name = String::from(signal_channel("prod", "wf-123")?);
160 let delivery = AionSurfaceError::SignalDeliveryFailed {
161 channel_name: channel_name.clone(),
162 workflow_id: "wf-123".to_owned(),
163 signal_name: "approve".to_owned(),
164 message: "publish failed".to_owned(),
165 };
166 let validation = AionSurfaceError::SignalValidationFailed {
167 channel_name: channel_name.clone(),
168 workflow_id: "wf-123".to_owned(),
169 signal_name: "approve".to_owned(),
170 message: "payload schema mismatch".to_owned(),
171 };
172
173 for display in [delivery.to_string(), validation.to_string()] {
174 assert!(display.contains(&channel_name));
175 assert!(display.contains("wf-123"));
176 assert!(display.contains("approve"));
177 }
178
179 Ok(())
180 }
181
182 #[test]
183 fn streaming_display_includes_channel_and_workflow_context() -> Result<(), AionSurfaceError> {
184 let channel_name = String::from(history_channel("prod", "wf-123")?);
185 let error = AionSurfaceError::StreamingFailed {
186 channel_name: channel_name.clone(),
187 workflow_id: "wf-123".to_owned(),
188 message: "subscribe failed".to_owned(),
189 };
190 let display = error.to_string();
191
192 assert!(display.contains(&channel_name));
193 assert!(display.contains("wf-123"));
194 assert!(display.contains("subscribe failed"));
195
196 Ok(())
197 }
198
199 #[test]
200 fn invalid_channel_name_display_identifies_invalid_input() {
201 let error = AionSurfaceError::InvalidChannelName {
202 part: "namespace".to_owned(),
203 input: "bad.ns".to_owned(),
204 message: "must not contain dots".to_owned(),
205 };
206 let display = error.to_string();
207
208 assert!(display.contains("namespace"));
209 assert!(display.contains("bad.ns"));
210 assert!(display.contains("must not contain dots"));
211 }
212}