1#![allow(non_snake_case)]
2
3use serde::{Deserialize, Serialize};
4
5pub type RunId = String;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum StepStatus {
12 Pending,
13 InProgress,
14 Completed,
15 Failed,
16}
17
18pub type StepPath = Vec<String>;
20
21#[derive(Debug, Clone)]
22pub struct HarnessExecContext {
23 pub run_id: RunId,
24 pub step_path: StepPath,
25 pub exec_ordinal: usize,
26}
27
28pub mod safe_boundary_types {
33 pub const BEFORE_STEP_START: &str = "before-step-start";
34 pub const AFTER_STEP_COMPLETE: &str = "after-step-complete";
35 pub const BEFORE_CONDITIONAL_BODY: &str = "before-conditional-body";
36 pub const AFTER_CONDITIONAL_BODY: &str = "after-conditional-body";
37 pub const BEFORE_LOOP_ITERATION: &str = "before-loop-iteration";
38 pub const AFTER_LOOP_ITERATION: &str = "after-loop-iteration";
39 pub const AFTER_BRANCH_TRANSITION: &str = "after-branch-transition";
40 pub const BEFORE_JOIN: &str = "before-join";
41 pub const AFTER_JOIN: &str = "after-join";
42 pub const BEFORE_MATCH_ARM: &str = "before-match-arm";
43 pub const AFTER_MATCH_ARM: &str = "after-match-arm";
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(tag = "type")]
53pub enum ExecutionEvent {
54 StepStarted {
55 runId: RunId,
56 stepPath: StepPath,
57 },
58 StepCompleted {
59 runId: RunId,
60 stepPath: StepPath,
61 },
62 StepFailed {
63 runId: RunId,
64 stepPath: StepPath,
65 error: String,
66 },
67 CheckEvaluated {
68 runId: RunId,
69 checkName: String,
70 result: bool,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 reason: Option<String>,
73 },
74 MatchEvaluated {
75 runId: RunId,
76 checkName: String,
77 variant: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 reason: Option<String>,
80 armIndex: Option<i64>,
81 },
82 BranchStarted {
83 runId: RunId,
84 branchPath: StepPath,
85 },
86 BranchCompleted {
87 runId: RunId,
88 branchPath: StepPath,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 output: Option<serde_json::Value>,
91 },
92 BranchFailed {
93 runId: RunId,
94 branchPath: StepPath,
95 error: String,
96 },
97 JoinStarted {
98 runId: RunId,
99 joinWorkflow: String,
100 },
101 RunPaused {
102 runId: RunId,
103 position: StepPath,
104 },
105 RunCompleted {
106 runId: RunId,
107 },
108 RunFailed {
109 runId: RunId,
110 position: StepPath,
111 error: String,
112 },
113 SafeBoundary {
114 runId: RunId,
115 boundaryType: String,
116 stepPath: StepPath,
117 },
118}
119
120impl ExecutionEvent {
121 pub fn event_type(&self) -> &'static str {
123 match self {
124 ExecutionEvent::StepStarted { .. } => "StepStarted",
125 ExecutionEvent::StepCompleted { .. } => "StepCompleted",
126 ExecutionEvent::StepFailed { .. } => "StepFailed",
127 ExecutionEvent::CheckEvaluated { .. } => "CheckEvaluated",
128 ExecutionEvent::MatchEvaluated { .. } => "MatchEvaluated",
129 ExecutionEvent::BranchStarted { .. } => "BranchStarted",
130 ExecutionEvent::BranchCompleted { .. } => "BranchCompleted",
131 ExecutionEvent::BranchFailed { .. } => "BranchFailed",
132 ExecutionEvent::JoinStarted { .. } => "JoinStarted",
133 ExecutionEvent::RunPaused { .. } => "RunPaused",
134 ExecutionEvent::RunCompleted { .. } => "RunCompleted",
135 ExecutionEvent::RunFailed { .. } => "RunFailed",
136 ExecutionEvent::SafeBoundary { .. } => "SafeBoundary",
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct BranchOutput {
144 pub workflow: String,
145 pub output: Option<String>,
146}
147
148pub type HarnessDispatchFn = std::sync::Arc<
150 dyn Fn(
151 &crate::parser::ast::ExecBlock,
152 HarnessExecContext,
153 ) -> std::pin::Pin<
154 Box<
155 dyn std::future::Future<Output = Result<crate::harness::types::ExecResult, String>>
156 + Send,
157 >,
158 > + Send
159 + Sync,
160>;
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct StepState {
165 pub stepPath: StepPath,
166 pub status: StepStatus,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172pub enum RunStatus {
173 Running,
174 Paused,
175 Completed,
176 Failed,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct RunState {
182 pub runId: RunId,
183 pub rootWorkflow: String,
184 pub status: RunStatus,
185 pub steps: Vec<StepState>,
186 pub events: Vec<ExecutionEvent>,
187 pub safeBoundaries: Vec<usize>,
189 pub lastSafeBoundaryIndex: i64,
192}
193
194pub type OnEventCallback = Box<dyn Fn(&ExecutionEvent) + Send + Sync>;
196
197pub type OnSaveCallback = Box<dyn Fn(&RunState) + Send + Sync>;
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_step_status_serialization() {
206 assert_eq!(
207 serde_json::to_string(&StepStatus::Pending).unwrap(),
208 "\"pending\""
209 );
210 assert_eq!(
211 serde_json::to_string(&StepStatus::InProgress).unwrap(),
212 "\"in_progress\""
213 );
214 assert_eq!(
215 serde_json::to_string(&StepStatus::Completed).unwrap(),
216 "\"completed\""
217 );
218 assert_eq!(
219 serde_json::to_string(&StepStatus::Failed).unwrap(),
220 "\"failed\""
221 );
222 }
223
224 #[test]
225 fn test_step_status_deserialization() {
226 let pending: StepStatus = serde_json::from_str("\"pending\"").unwrap();
227 assert_eq!(pending, StepStatus::Pending);
228 let in_progress: StepStatus = serde_json::from_str("\"in_progress\"").unwrap();
229 assert_eq!(in_progress, StepStatus::InProgress);
230 }
231
232 #[test]
233 fn test_run_status_serialization() {
234 assert_eq!(
235 serde_json::to_string(&RunStatus::Running).unwrap(),
236 "\"running\""
237 );
238 assert_eq!(
239 serde_json::to_string(&RunStatus::Paused).unwrap(),
240 "\"paused\""
241 );
242 assert_eq!(
243 serde_json::to_string(&RunStatus::Completed).unwrap(),
244 "\"completed\""
245 );
246 assert_eq!(
247 serde_json::to_string(&RunStatus::Failed).unwrap(),
248 "\"failed\""
249 );
250 }
251
252 #[test]
253 fn test_execution_event_step_started_json() {
254 let event = ExecutionEvent::StepStarted {
255 runId: "run-1".to_string(),
256 stepPath: vec!["main".to_string(), "deploy".to_string()],
257 };
258 assert_eq!(event.event_type(), "StepStarted");
259 let json = serde_json::to_string(&event).unwrap();
260 assert!(json.contains("\"type\":\"StepStarted\""), "json: {}", json);
261 assert!(json.contains("\"runId\":\"run-1\""), "json: {}", json);
262 assert!(
263 json.contains("\"stepPath\":[\"main\",\"deploy\"]"),
264 "json: {}",
265 json
266 );
267 }
268
269 #[test]
270 fn test_execution_event_step_failed_json() {
271 let event = ExecutionEvent::StepFailed {
272 runId: "run-1".to_string(),
273 stepPath: vec!["main".to_string()],
274 error: "something broke".to_string(),
275 };
276 assert_eq!(event.event_type(), "StepFailed");
277 let json = serde_json::to_string(&event).unwrap();
278 assert!(
279 json.contains("\"error\":\"something broke\""),
280 "json: {}",
281 json
282 );
283 }
284
285 #[test]
286 fn test_execution_event_check_evaluated_with_reason() {
287 let event = ExecutionEvent::CheckEvaluated {
288 runId: "run-1".to_string(),
289 checkName: "is-ready".to_string(),
290 result: true,
291 reason: Some("all checks passed".to_string()),
292 };
293 assert_eq!(event.event_type(), "CheckEvaluated");
294 let json = serde_json::to_string(&event).unwrap();
295 assert!(
296 json.contains("\"checkName\":\"is-ready\""),
297 "json: {}",
298 json
299 );
300 assert!(json.contains("\"result\":true"), "json: {}", json);
301 assert!(
302 json.contains("\"reason\":\"all checks passed\""),
303 "json: {}",
304 json
305 );
306 }
307
308 #[test]
309 fn test_execution_event_check_evaluated_without_reason() {
310 let event = ExecutionEvent::CheckEvaluated {
311 runId: "run-1".to_string(),
312 checkName: "is-ready".to_string(),
313 result: false,
314 reason: None,
315 };
316 let json = serde_json::to_string(&event).unwrap();
317 assert!(
318 !json.contains("reason"),
319 "reason should be omitted: {}",
320 json
321 );
322 }
323
324 #[test]
325 fn test_execution_event_branch_completed_with_output() {
326 let event = ExecutionEvent::BranchCompleted {
327 runId: "run-1".to_string(),
328 branchPath: vec!["par".to_string(), "branch-a".to_string()],
329 output: Some(serde_json::json!({"status": "ok"})),
330 };
331 assert_eq!(event.event_type(), "BranchCompleted");
332 let json = serde_json::to_string(&event).unwrap();
333 assert!(
334 json.contains("\"output\":{\"status\":\"ok\"}"),
335 "json: {}",
336 json
337 );
338 }
339
340 #[test]
341 fn test_execution_event_branch_completed_without_output() {
342 let event = ExecutionEvent::BranchCompleted {
343 runId: "run-1".to_string(),
344 branchPath: vec!["par".to_string()],
345 output: None,
346 };
347 let json = serde_json::to_string(&event).unwrap();
348 assert!(
349 !json.contains("output"),
350 "output should be omitted: {}",
351 json
352 );
353 }
354
355 #[test]
356 fn test_execution_event_safe_boundary_json() {
357 let event = ExecutionEvent::SafeBoundary {
358 runId: "run-1".to_string(),
359 boundaryType: safe_boundary_types::BEFORE_STEP_START.to_string(),
360 stepPath: vec!["main".to_string()],
361 };
362 assert_eq!(event.event_type(), "SafeBoundary");
363 let json = serde_json::to_string(&event).unwrap();
364 assert!(
365 json.contains("\"boundaryType\":\"before-step-start\""),
366 "json: {}",
367 json
368 );
369 }
370
371 #[test]
372 fn test_execution_event_run_completed() {
373 let event = ExecutionEvent::RunCompleted {
374 runId: "run-1".to_string(),
375 };
376 assert_eq!(event.event_type(), "RunCompleted");
377 let json = serde_json::to_string(&event).unwrap();
378 assert!(json.contains("\"type\":\"RunCompleted\""), "json: {}", json);
379 assert!(json.contains("\"runId\":\"run-1\""), "json: {}", json);
380 }
381
382 #[test]
383 fn test_execution_event_all_types() {
384 let events: Vec<ExecutionEvent> = vec![
386 ExecutionEvent::StepStarted {
387 runId: "r".to_string(),
388 stepPath: vec![],
389 },
390 ExecutionEvent::StepCompleted {
391 runId: "r".to_string(),
392 stepPath: vec![],
393 },
394 ExecutionEvent::StepFailed {
395 runId: "r".to_string(),
396 stepPath: vec![],
397 error: "e".to_string(),
398 },
399 ExecutionEvent::CheckEvaluated {
400 runId: "r".to_string(),
401 checkName: "c".to_string(),
402 result: true,
403 reason: None,
404 },
405 ExecutionEvent::MatchEvaluated {
406 runId: "r".to_string(),
407 checkName: "c".to_string(),
408 variant: "v".to_string(),
409 reason: None,
410 armIndex: None,
411 },
412 ExecutionEvent::BranchStarted {
413 runId: "r".to_string(),
414 branchPath: vec![],
415 },
416 ExecutionEvent::BranchCompleted {
417 runId: "r".to_string(),
418 branchPath: vec![],
419 output: None,
420 },
421 ExecutionEvent::BranchFailed {
422 runId: "r".to_string(),
423 branchPath: vec![],
424 error: "e".to_string(),
425 },
426 ExecutionEvent::JoinStarted {
427 runId: "r".to_string(),
428 joinWorkflow: "w".to_string(),
429 },
430 ExecutionEvent::RunPaused {
431 runId: "r".to_string(),
432 position: vec![],
433 },
434 ExecutionEvent::RunCompleted {
435 runId: "r".to_string(),
436 },
437 ExecutionEvent::RunFailed {
438 runId: "r".to_string(),
439 position: vec![],
440 error: "e".to_string(),
441 },
442 ExecutionEvent::SafeBoundary {
443 runId: "r".to_string(),
444 boundaryType: "before-step-start".to_string(),
445 stepPath: vec![],
446 },
447 ];
448 let expected_types = [
449 "StepStarted",
450 "StepCompleted",
451 "StepFailed",
452 "CheckEvaluated",
453 "MatchEvaluated",
454 "BranchStarted",
455 "BranchCompleted",
456 "BranchFailed",
457 "JoinStarted",
458 "RunPaused",
459 "RunCompleted",
460 "RunFailed",
461 "SafeBoundary",
462 ];
463 for (event, expected) in events.iter().zip(expected_types.iter()) {
464 assert_eq!(event.event_type(), *expected);
465 }
466 }
467
468 #[test]
469 fn test_step_state_serialization() {
470 let state = StepState {
471 stepPath: vec!["main".to_string(), "step-1".to_string()],
472 status: StepStatus::Completed,
473 };
474 let json = serde_json::to_string(&state).unwrap();
475 assert!(
476 json.contains("\"stepPath\":[\"main\",\"step-1\"]"),
477 "json: {}",
478 json
479 );
480 assert!(json.contains("\"status\":\"completed\""), "json: {}", json);
481 }
482
483 #[test]
484 fn test_run_state_serialization() {
485 let state = RunState {
486 runId: "run-123".to_string(),
487 rootWorkflow: "main".to_string(),
488 status: RunStatus::Running,
489 steps: vec![StepState {
490 stepPath: vec!["main".to_string()],
491 status: StepStatus::Pending,
492 }],
493 events: vec![],
494 safeBoundaries: vec![],
495 lastSafeBoundaryIndex: -1,
496 };
497 let json = serde_json::to_string(&state).unwrap();
498 assert!(json.contains("\"runId\":\"run-123\""), "json: {}", json);
499 assert!(json.contains("\"rootWorkflow\":\"main\""), "json: {}", json);
500 assert!(json.contains("\"status\":\"running\""), "json: {}", json);
501 assert!(
502 json.contains("\"lastSafeBoundaryIndex\":-1"),
503 "json: {}",
504 json
505 );
506 }
507
508 #[test]
509 fn test_run_state_roundtrip() {
510 let state = RunState {
511 runId: "run-abc".to_string(),
512 rootWorkflow: "deploy".to_string(),
513 status: RunStatus::Completed,
514 steps: vec![
515 StepState {
516 stepPath: vec!["deploy".to_string(), "build".to_string()],
517 status: StepStatus::Completed,
518 },
519 StepState {
520 stepPath: vec!["deploy".to_string(), "test".to_string()],
521 status: StepStatus::Failed,
522 },
523 ],
524 events: vec![
525 ExecutionEvent::StepStarted {
526 runId: "run-abc".to_string(),
527 stepPath: vec!["deploy".to_string(), "build".to_string()],
528 },
529 ExecutionEvent::SafeBoundary {
530 runId: "run-abc".to_string(),
531 boundaryType: safe_boundary_types::AFTER_STEP_COMPLETE.to_string(),
532 stepPath: vec!["deploy".to_string(), "build".to_string()],
533 },
534 ],
535 safeBoundaries: vec![1],
536 lastSafeBoundaryIndex: 1,
537 };
538 let json = serde_json::to_string(&state).unwrap();
539 let deserialized: RunState = serde_json::from_str(&json).unwrap();
540 assert_eq!(deserialized.runId, "run-abc");
541 assert_eq!(deserialized.rootWorkflow, "deploy");
542 assert_eq!(deserialized.status, RunStatus::Completed);
543 assert_eq!(deserialized.steps.len(), 2);
544 assert_eq!(deserialized.events.len(), 2);
545 assert_eq!(deserialized.safeBoundaries, vec![1]);
546 assert_eq!(deserialized.lastSafeBoundaryIndex, 1);
547 }
548
549 #[test]
550 fn test_safe_boundary_type_constants() {
551 assert_eq!(safe_boundary_types::BEFORE_STEP_START, "before-step-start");
553 assert_eq!(
554 safe_boundary_types::AFTER_STEP_COMPLETE,
555 "after-step-complete"
556 );
557 assert_eq!(
558 safe_boundary_types::BEFORE_CONDITIONAL_BODY,
559 "before-conditional-body"
560 );
561 assert_eq!(
562 safe_boundary_types::AFTER_CONDITIONAL_BODY,
563 "after-conditional-body"
564 );
565 assert_eq!(
566 safe_boundary_types::BEFORE_LOOP_ITERATION,
567 "before-loop-iteration"
568 );
569 assert_eq!(
570 safe_boundary_types::AFTER_LOOP_ITERATION,
571 "after-loop-iteration"
572 );
573 assert_eq!(
574 safe_boundary_types::AFTER_BRANCH_TRANSITION,
575 "after-branch-transition"
576 );
577 assert_eq!(safe_boundary_types::BEFORE_JOIN, "before-join");
578 assert_eq!(safe_boundary_types::AFTER_JOIN, "after-join");
579 assert_eq!(safe_boundary_types::BEFORE_MATCH_ARM, "before-match-arm");
580 assert_eq!(safe_boundary_types::AFTER_MATCH_ARM, "after-match-arm");
581 }
582
583 #[test]
584 fn test_match_evaluated_with_reason() {
585 let event = ExecutionEvent::MatchEvaluated {
586 runId: "r".to_string(),
587 checkName: "size-check".to_string(),
588 variant: "small".to_string(),
589 reason: Some("under 50 lines".to_string()),
590 armIndex: Some(0),
591 };
592 assert_eq!(event.event_type(), "MatchEvaluated");
593 let json = serde_json::to_string(&event).unwrap();
594 assert!(json.contains("\"variant\":\"small\""));
595 assert!(json.contains("\"reason\":\"under 50 lines\""));
596 assert!(json.contains("\"armIndex\":0"));
597 }
598
599 #[test]
600 fn test_match_evaluated_else_arm() {
601 let event = ExecutionEvent::MatchEvaluated {
602 runId: "r".to_string(),
603 checkName: "size-check".to_string(),
604 variant: "unknown".to_string(),
605 reason: None,
606 armIndex: None,
607 };
608 let json = serde_json::to_string(&event).unwrap();
609 assert!(
610 !json.contains("reason"),
611 "reason should be omitted: {}",
612 json
613 );
614 assert!(
615 json.contains("\"armIndex\":null"),
616 "armIndex should be null: {}",
617 json
618 );
619 }
620
621 #[test]
622 fn test_execution_event_deserialization() {
623 let json = r#"{"type":"StepStarted","runId":"run-1","stepPath":["main"]}"#;
624 let event: ExecutionEvent = serde_json::from_str(json).unwrap();
625 assert_eq!(event.event_type(), "StepStarted");
626 match event {
627 ExecutionEvent::StepStarted { runId, stepPath } => {
628 assert_eq!(runId, "run-1");
629 assert_eq!(stepPath, vec!["main".to_string()]);
630 }
631 _ => panic!("wrong variant"),
632 }
633 }
634
635 #[test]
636 fn test_join_started_serialization() {
637 let event = ExecutionEvent::JoinStarted {
638 runId: "r".to_string(),
639 joinWorkflow: "merge-results".to_string(),
640 };
641 let json = serde_json::to_string(&event).unwrap();
642 assert!(
643 json.contains("\"joinWorkflow\":\"merge-results\""),
644 "json: {}",
645 json
646 );
647 }
648
649 #[test]
650 fn test_run_paused_serialization() {
651 let event = ExecutionEvent::RunPaused {
652 runId: "r".to_string(),
653 position: vec!["main".to_string(), "step-3".to_string()],
654 };
655 let json = serde_json::to_string(&event).unwrap();
656 assert!(
657 json.contains("\"position\":[\"main\",\"step-3\"]"),
658 "json: {}",
659 json
660 );
661 }
662
663 #[test]
664 fn test_run_failed_serialization() {
665 let event = ExecutionEvent::RunFailed {
666 runId: "r".to_string(),
667 position: vec!["main".to_string()],
668 error: "timeout".to_string(),
669 };
670 let json = serde_json::to_string(&event).unwrap();
671 assert!(json.contains("\"type\":\"RunFailed\""), "json: {}", json);
672 assert!(json.contains("\"error\":\"timeout\""), "json: {}", json);
673 }
674
675 #[test]
676 fn test_branch_failed_serialization() {
677 let event = ExecutionEvent::BranchFailed {
678 runId: "r".to_string(),
679 branchPath: vec!["par".to_string(), "b1".to_string()],
680 error: "branch error".to_string(),
681 };
682 let json = serde_json::to_string(&event).unwrap();
683 assert!(json.contains("\"type\":\"BranchFailed\""), "json: {}", json);
684 assert!(
685 json.contains("\"branchPath\":[\"par\",\"b1\"]"),
686 "json: {}",
687 json
688 );
689 }
690
691 #[test]
692 fn test_branch_started_serialization() {
693 let event = ExecutionEvent::BranchStarted {
694 runId: "r".to_string(),
695 branchPath: vec!["par".to_string(), "b1".to_string()],
696 };
697 let json = serde_json::to_string(&event).unwrap();
698 assert!(
699 json.contains("\"type\":\"BranchStarted\""),
700 "json: {}",
701 json
702 );
703 }
704}