1use super::event::PipelinePhase;
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
15pub enum XmlOutputType {
16 DevelopmentResult,
18 DevelopmentPlan,
20 ReviewIssues,
22 FixResult,
24 CommitMessage,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
33pub struct XmlOutputContext {
34 pub iteration: Option<u32>,
36 pub pass: Option<u32>,
38 #[serde(default)]
43 pub snippets: Vec<XmlCodeSnippet>,
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
48pub struct XmlCodeSnippet {
49 pub file: String,
51 pub line_start: u32,
53 pub line_end: u32,
55 pub content: String,
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
64pub enum UIEvent {
65 PhaseTransition {
67 from: Option<PipelinePhase>,
68 to: PipelinePhase,
69 },
70
71 IterationProgress { current: u32, total: u32 },
73
74 ReviewProgress { pass: u32, total: u32 },
76
77 AgentActivity { agent: String, message: String },
79
80 PushCompleted {
82 remote: String,
83 branch: String,
84 commit_sha: String,
85 },
86
87 PushFailed {
91 remote: String,
92 branch: String,
93 error: String,
94 },
95
96 PullRequestCreated { url: String, number: u32 },
98
99 PullRequestFailed { error: String },
103
104 XmlOutput {
109 xml_type: XmlOutputType,
111 content: String,
113 context: Option<XmlOutputContext>,
115 },
116
117 PromptReplayHit {
124 key: String,
126 was_replayed: bool,
129 },
130}
131
132impl UIEvent {
133 #[must_use]
135 pub const fn phase_emoji(phase: &PipelinePhase) -> &'static str {
136 match phase {
137 PipelinePhase::Planning => "π",
138 PipelinePhase::Development => "π¨",
139 PipelinePhase::Review => "π",
140 PipelinePhase::CommitMessage => "π",
141 PipelinePhase::FinalValidation => "β
",
142 PipelinePhase::Finalizing => "π",
143 PipelinePhase::Complete => "π",
144 PipelinePhase::AwaitingDevFix => "π§",
145 PipelinePhase::Interrupted => "βΈοΈ",
146 }
147 }
148
149 #[must_use]
154 pub fn format_for_display(&self) -> String {
155 crate::rendering::render_ui_event(self)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_phase_transition_display() {
165 let event = UIEvent::PhaseTransition {
166 from: Some(PipelinePhase::Planning),
167 to: PipelinePhase::Development,
168 };
169 let display = event.format_for_display();
170 assert!(display.contains("π¨"));
171 assert!(display.contains("Development"));
172 }
173
174 #[test]
175 fn test_iteration_progress_display() {
176 let event = UIEvent::IterationProgress {
177 current: 2,
178 total: 5,
179 };
180 let display = event.format_for_display();
181 assert!(display.contains("2/5"));
182 }
183
184 #[test]
185 fn test_review_progress_display() {
186 let event = UIEvent::ReviewProgress { pass: 1, total: 3 };
187 let display = event.format_for_display();
188 assert!(display.contains("1/3"));
189 assert!(display.contains("Review pass"));
190 }
191
192 #[test]
193 fn test_agent_activity_display() {
194 let event = UIEvent::AgentActivity {
195 agent: "claude".to_string(),
196 message: "Processing request".to_string(),
197 };
198 let display = event.format_for_display();
199 assert!(display.contains("[claude]"));
200 assert!(display.contains("Processing request"));
201 }
202
203 #[test]
204 fn test_ui_event_serialization() {
205 let event = UIEvent::PhaseTransition {
206 from: None,
207 to: PipelinePhase::Planning,
208 };
209 let json = serde_json::to_string(&event).unwrap();
210 let deserialized: UIEvent = serde_json::from_str(&json).unwrap();
211 assert_eq!(event, deserialized);
212 }
213
214 #[test]
215 fn test_phase_emoji_all_phases() {
216 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::Planning), "π");
218 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::Development), "π¨");
219 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::Review), "π");
220 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::CommitMessage), "π");
221 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::FinalValidation), "β
");
222 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::Finalizing), "π");
223 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::Complete), "π");
224 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::AwaitingDevFix), "π§");
225 assert_eq!(UIEvent::phase_emoji(&PipelinePhase::Interrupted), "βΈοΈ");
226 }
227
228 #[test]
229 fn test_prompt_replay_hit_replayed_display() {
230 let event = UIEvent::PromptReplayHit {
231 key: "planning_1".to_string(),
232 was_replayed: true,
233 };
234 let display = event.format_for_display();
235 assert!(display.contains("planning_1"));
236 assert!(
237 display.contains("Replayed")
238 || display.contains("replay")
239 || display.contains("stored")
240 );
241 }
242
243 #[test]
244 fn test_prompt_replay_hit_fresh_display() {
245 let event = UIEvent::PromptReplayHit {
246 key: "development_2".to_string(),
247 was_replayed: false,
248 };
249 let display = event.format_for_display();
250 assert!(display.contains("development_2"));
251 assert!(
252 display.contains("fresh")
253 || display.contains("Generated")
254 || display.contains("prompt")
255 );
256 }
257
258 #[test]
259 fn test_prompt_replay_hit_serialization() {
260 let event = UIEvent::PromptReplayHit {
261 key: "commit_message_attempt_iter1_1".to_string(),
262 was_replayed: false,
263 };
264 let json = serde_json::to_string(&event).unwrap();
265 let deserialized: UIEvent = serde_json::from_str(&json).unwrap();
266 assert_eq!(event, deserialized);
267 }
268
269 #[test]
270 fn test_phase_transition_from_none() {
271 let event = UIEvent::PhaseTransition {
273 from: None,
274 to: PipelinePhase::Planning,
275 };
276 let display = event.format_for_display();
277 assert!(display.contains("π"));
278 assert!(display.contains("Planning"));
279 }
280
281 #[test]
286 fn test_xml_output_type_serialization() {
287 let xml_type = XmlOutputType::DevelopmentResult;
288 let json = serde_json::to_string(&xml_type).unwrap();
289 let deserialized: XmlOutputType = serde_json::from_str(&json).unwrap();
290 assert_eq!(xml_type, deserialized);
291 }
292
293 #[test]
294 fn test_xml_output_context_default() {
295 let context = XmlOutputContext::default();
296 assert!(context.iteration.is_none());
297 assert!(context.pass.is_none());
298 assert!(context.snippets.is_empty());
299 }
300
301 #[test]
302 fn test_xml_output_context_with_values() {
303 let context = XmlOutputContext {
304 iteration: Some(2),
305 pass: Some(1),
306 snippets: Vec::new(),
307 };
308 assert_eq!(context.iteration, Some(2));
309 assert_eq!(context.pass, Some(1));
310 }
311
312 #[test]
313 fn test_xml_output_event_serialization() {
314 let event = UIEvent::XmlOutput {
315 xml_type: XmlOutputType::ReviewIssues,
316 content: "<ralph-issues><ralph-issue>Test</ralph-issue></ralph-issues>".to_string(),
317 context: Some(XmlOutputContext {
318 iteration: None,
319 pass: Some(1),
320 snippets: Vec::new(),
321 }),
322 };
323 let json = serde_json::to_string(&event).unwrap();
324 let deserialized: UIEvent = serde_json::from_str(&json).unwrap();
325 assert_eq!(event, deserialized);
326 }
327
328 #[test]
329 fn test_xml_output_types_all_variants() {
330 let variants = [
332 XmlOutputType::DevelopmentResult,
333 XmlOutputType::DevelopmentPlan,
334 XmlOutputType::ReviewIssues,
335 XmlOutputType::FixResult,
336 XmlOutputType::CommitMessage,
337 ];
338 assert!(
339 variants.iter().enumerate().all(|(i, v1)| {
340 variants
341 .iter()
342 .enumerate()
343 .all(|(j, v2)| i == j || v1 != v2)
344 }),
345 "All XmlOutputType variants should be distinct"
346 );
347 }
348}