1use std::collections::HashMap;
35use std::sync::Arc;
36
37use chrono::Utc;
38use crossbeam_channel::{Receiver, Sender};
39use rite_model::{ActId, ActionType, ArtifactId, Ceremony, MaterialId, OutputId, ParamId, RoleId};
40use rite_sdk::{Backend, BackendError};
41use thiserror::Error;
42
43use crate::actions::ActionMetadata;
44use crate::backend::BackendRegistry;
45use crate::executor::{
46 ExecutionError, load_material_artifact, step_info_from, write_artifact_to_disk,
47};
48use crate::expressions;
49use crate::output_config::OutputConfig;
50use crate::protocol::{ExecEvent, Icon, MaterialOverview, UiCommand, UiSignal};
51use crate::reporter::{Reporter, ReporterError};
52use crate::state::{ExecutionState, HandlerContext, StepResult};
53use crate::step_info::StepInfo;
54use crate::system_info::StartupSnapshot;
55use crate::transcript_sink::{TranscriptFingerprint, TranscriptSink};
56use rite_model::{ErrorRecord, Prompt, StepFact, StepOutcome};
57
58#[derive(Debug, Error)]
60pub enum ActionError {
61 #[error("ceremony aborted by operator")]
64 Aborted,
65 #[error("frontend channel disconnected")]
67 Disconnected,
68 #[error("transcript write failed: {0}")]
70 Transcript(std::io::Error),
71 #[error("backend error: {0}")]
73 Backend(#[from] BackendError),
74 #[error("{0}")]
76 Failed(String),
77}
78
79impl From<ReporterError> for ActionError {
80 fn from(value: ReporterError) -> Self {
81 match value {
82 ReporterError::Aborted => ActionError::Aborted,
83 ReporterError::Disconnected => ActionError::Disconnected,
84 ReporterError::Transcript(e) => ActionError::Transcript(e),
85 ReporterError::NoCurrentStep(_) => ActionError::Failed(value.to_string()),
86 }
87 }
88}
89
90pub fn parse_params<P: serde::de::DeserializeOwned>(
99 params: &serde_json::Value,
100) -> Result<P, ActionError> {
101 serde_json::from_value(params.clone())
102 .map_err(|e| ActionError::Failed(format!("invalid params: {e}")))
103}
104
105impl From<ReporterError> for ExecutionError {
106 fn from(value: ReporterError) -> Self {
107 match value {
108 ReporterError::Aborted => ExecutionError::StepAborted(rite_model::StepId::new("")),
109 ReporterError::Disconnected => {
110 ExecutionError::TranscriptError("frontend channel disconnected".to_string())
111 }
112 ReporterError::Transcript(e) => ExecutionError::TranscriptError(e.to_string()),
113 ReporterError::NoCurrentStep(_) => ExecutionError::TranscriptError(value.to_string()),
114 }
115 }
116}
117
118pub trait Action: Send + Sync {
125 fn metadata(&self) -> ActionMetadata;
127
128 fn apply_defaults(&self, _params: &mut serde_json::Value, _step: &StepInfo) {}
130
131 fn execute(
138 &self,
139 step: &StepInfo,
140 ctx: &HandlerContext,
141 params: &serde_json::Value,
142 reporter: &mut Reporter<'_>,
143 backend: Option<&mut dyn Backend>,
144 ) -> Result<StepResult, ActionError>;
145}
146
147#[derive(Default)]
149pub struct ActionRegistry {
150 actions: HashMap<ActionType, Arc<dyn Action>>,
151}
152
153impl ActionRegistry {
154 #[must_use]
156 pub fn new() -> Self {
157 Self::default()
158 }
159
160 pub fn register(&mut self, action: Arc<dyn Action>) {
162 let action_type = action.metadata().action_type;
163 self.actions.insert(action_type, action);
164 }
165
166 #[must_use]
168 pub fn get(&self, action: &ActionType) -> Option<&Arc<dyn Action>> {
169 self.actions.get(action)
170 }
171
172 pub fn action_types(&self) -> impl Iterator<Item = &ActionType> {
174 self.actions.keys()
175 }
176
177 #[must_use]
181 pub fn unsupported_actions(&self, steps: &[rite_model::Step]) -> Vec<ActionType> {
182 let mut seen = std::collections::HashSet::new();
183 let mut result = Vec::new();
184 for step in steps {
185 if !self.actions.contains_key(&step.action) && seen.insert(step.action) {
186 result.push(step.action);
187 }
188 }
189 result
190 }
191}
192
193#[derive(Debug)]
195pub struct ExecutionSummary {
196 pub ceremony_name: String,
198 pub steps_completed: usize,
200 pub transcript_fingerprint: TranscriptFingerprint,
202}
203
204pub struct Executor {
206 ceremony: Ceremony,
207 registry: ActionRegistry,
208 backend_registry: BackendRegistry,
209 output_config: OutputConfig,
210 dry_run: bool,
211 startup: StartupSnapshot,
212}
213
214impl Executor {
215 #[must_use]
221 pub fn new(
222 ceremony: Ceremony,
223 registry: ActionRegistry,
224 backend_registry: BackendRegistry,
225 output_config: OutputConfig,
226 dry_run: bool,
227 startup: StartupSnapshot,
228 ) -> Self {
229 Self {
230 ceremony,
231 registry,
232 backend_registry,
233 output_config,
234 dry_run,
235 startup,
236 }
237 }
238
239 pub fn run(
252 self,
253 cmd_rx: &Receiver<UiCommand>,
254 event_tx: &Sender<ExecEvent>,
255 mut transcript_sink: Box<dyn TranscriptSink>,
256 ) -> Result<ExecutionSummary, ExecutionError> {
257 validate_parameters(&self.ceremony)?;
258
259 let resolved_params: HashMap<ParamId, serde_json::Value> = self
260 .ceremony
261 .parameters
262 .iter()
263 .map(|(id, p)| (id.clone(), p.value.clone()))
264 .collect();
265
266 let roles: HashMap<RoleId, String> = self
267 .ceremony
268 .roles
269 .iter()
270 .map(|(id, r)| (id.clone(), r.name.clone()))
271 .collect();
272
273 let materials_map: HashMap<MaterialId, String> = self
274 .ceremony
275 .materials
276 .iter()
277 .map(|(id, m)| (id.clone(), m.display_name().to_string()))
278 .collect();
279
280 let ceremony_name = self.ceremony.metadata.name.clone();
281 let mut reporter = Reporter::new(event_tx, cmd_rx, transcript_sink.as_mut());
282
283 let outcome = self.execute_inner(&mut reporter, resolved_params, roles, materials_map);
284
285 match outcome {
290 Ok(counts) => {
291 let completed = StepFact::CeremonyCompleted {
292 completed_at: Utc::now(),
293 };
294 transcript_sink
295 .record(&completed)
296 .map_err(|e| ExecutionError::TranscriptError(e.to_string()))?;
297 let _ = event_tx.send(ExecEvent::Fact(completed));
298 let fingerprint = transcript_sink
299 .finalize()
300 .map_err(|e| ExecutionError::TranscriptError(e.to_string()))?;
301 let _ = event_tx.send(ExecEvent::Finalized {
302 fingerprint: fingerprint.as_str().to_string(),
303 });
304 Ok(ExecutionSummary {
305 ceremony_name,
306 steps_completed: counts.completed,
307 transcript_fingerprint: fingerprint,
308 })
309 }
310 Err(err) => {
311 let record = err.to_error_record();
312 let failed = StepFact::CeremonyFailed {
313 error: record,
314 failed_at: Utc::now(),
315 };
316 let _ = transcript_sink.record(&failed);
317 let _ = event_tx.send(ExecEvent::Fact(failed));
318 if let Ok(fingerprint) = transcript_sink.finalize() {
319 let _ = event_tx.send(ExecEvent::Finalized {
320 fingerprint: fingerprint.as_str().to_string(),
321 });
322 }
323 Err(err)
324 }
325 }
326 }
327
328 #[allow(clippy::too_many_lines)]
329 fn execute_inner(
330 mut self,
331 reporter: &mut Reporter<'_>,
332 resolved_params: HashMap<ParamId, serde_json::Value>,
333 roles: HashMap<RoleId, String>,
334 materials_map: HashMap<MaterialId, String>,
335 ) -> Result<StepCounts, ExecutionError> {
336 let dry_run = self.dry_run;
337
338 reporter.fact(StepFact::CeremonyStarted {
339 name: self.ceremony.metadata.name.clone(),
340 started_at: Utc::now(),
341 })?;
342
343 reporter.signal(UiSignal::CeremonyOverview {
348 description: self.ceremony.metadata.description.clone(),
349 materials: self
350 .ceremony
351 .materials
352 .iter()
353 .map(|(_, m)| MaterialOverview::from_material(m))
354 .collect(),
355 step_count: self.ceremony.execution_plan.len(),
356 })?;
357
358 reporter.signal(UiSignal::SystemInfo(Box::new(self.startup.system.clone())))?;
363 reporter.signal(UiSignal::Environment(self.startup.environment.clone()))?;
364
365 if !dry_run {
370 reporter.prompt(&Prompt::Continue {
371 hint: Some("Press Enter to start the ceremony".to_string()),
372 })?;
373 }
374
375 let mut state = ExecutionState::new(resolved_params, roles, materials_map, dry_run);
376
377 for (id, material) in self.ceremony.materials.iter() {
379 let artifact = load_material_artifact(id.as_str(), material)?;
380 state = state.with_material(ArtifactId::new(id.as_str()), artifact);
381 let display_name = material.display_name();
382 reporter.log(Icon::Checkmark, format!("Loaded material: {display_name}"))?;
383 }
384
385 let has_acts = !self.ceremony.acts.is_empty();
386 let mut current_act: Option<ActId> = None;
387 let mut counts = StepCounts::default();
388
389 let plan = std::mem::take(&mut self.ceremony.execution_plan);
392 for step in &plan {
393 if has_acts {
395 let step_act = self
396 .ceremony
397 .sections
398 .get(&step.section)
399 .and_then(|s| s.act.clone());
400 if step_act != current_act {
401 if let Some(act_id) = &step_act
402 && let Some(act) = self.ceremony.acts.get(act_id)
403 {
404 reporter.fact(StepFact::ActStarted {
405 id: act_id.clone(),
406 label: act
407 .name
408 .clone()
409 .unwrap_or_else(|| act_id.as_str().to_string()),
410 })?;
411 }
412 current_act = step_act;
413 }
414 }
415
416 reporter.set_current_step(Some(step.id.clone()));
417 let started_at = Utc::now();
418 let role_id = step.role.clone().unwrap_or_else(|| RoleId::new(""));
419 let role_name = self
420 .ceremony
421 .roles
422 .get(&role_id)
423 .map_or_else(|| role_id.as_str().to_string(), |r| r.name.clone());
424 reporter.fact(StepFact::StepStarted {
425 id: step.id.clone(),
426 label: step.step_label.clone(),
427 role: role_id,
428 role_name,
429 started_at,
430 })?;
431
432 if !step.silent && !dry_run {
439 reporter.prompt(&Prompt::Continue {
440 hint: Some(format!("Press Enter to start step {}", step.step_label)),
441 })?;
442 }
443
444 let handler = self
446 .registry
447 .get(&step.action)
448 .ok_or(ExecutionError::UnknownAction(step.action))?
449 .clone();
450
451 let ctx = state.handler_context();
452 let step_info = step_info_from(step);
453 let mut params = expressions::evaluate_expr_value(&step.with, &ctx)?;
454 handler.apply_defaults(&mut params, &step_info);
455
456 let backend = if let Some(name) = &step_info.backend {
457 Some(self.backend_registry.get_mut(name).map_err(|e| {
458 ExecutionError::StepFailed {
459 step: step_info.id.clone(),
460 reason: e.to_string(),
461 }
462 })?)
463 } else {
464 None
465 };
466
467 let result = handler
468 .execute(&step_info, &ctx, ¶ms, reporter, backend)
469 .map_err(|err| match err {
470 ActionError::Aborted => ExecutionError::StepAborted(step.id.clone()),
471 ActionError::Disconnected => ExecutionError::StepFailed {
472 step: step.id.clone(),
473 reason: "frontend channel disconnected".to_string(),
474 },
475 ActionError::Transcript(e) => ExecutionError::TranscriptError(e.to_string()),
476 ActionError::Backend(e) => ExecutionError::BackendError(e),
477 ActionError::Failed(reason) => ExecutionError::StepFailed {
478 step: step.id.clone(),
479 reason,
480 },
481 })?;
482
483 let completed_at = Utc::now();
484
485 reporter.fact(StepFact::StepCompleted {
487 id: step.id.clone(),
488 outcome: result.outcome.clone(),
489 completed_at,
490 })?;
491
492 if let StepOutcome::Completed { .. } = &result.outcome {
493 counts.completed = counts.completed.saturating_add(1);
494 }
495
496 state = merge_step_result(state, result);
497
498 if let Some(artifact_id) = &step.creates
502 && !dry_run
503 {
504 let output_id = OutputId::new(artifact_id.as_str());
505 if self.ceremony.outputs.contains(&output_id) {
506 std::fs::create_dir_all(self.output_config.artifacts_dir()).map_err(|e| {
507 ExecutionError::OutputWriteFailed {
508 name: "artifacts directory".to_string(),
509 reason: e.to_string(),
510 }
511 })?;
512
513 let artifact_value = state.artifacts.get(artifact_id).ok_or_else(|| {
514 ExecutionError::OutputWriteFailed {
515 name: artifact_id.as_str().to_string(),
516 reason: "artifact not produced".to_string(),
517 }
518 })?;
519
520 let (path, hash, _size, _mime_type) =
521 write_artifact_to_disk(artifact_id, artifact_value, &self.output_config)?;
522
523 reporter.fact(StepFact::ArtifactWritten {
524 step: step.id.clone(),
525 name: artifact_id.as_str().to_string(),
526 path: path.clone(),
527 sha256: hash,
528 })?;
529 }
530 }
531 }
532
533 reporter.set_current_step(None);
534 drop(plan);
535
536 Ok(counts)
537 }
538}
539
540#[derive(Default, Debug, Clone, Copy)]
541struct StepCounts {
542 completed: usize,
543}
544
545impl ExecutionError {
546 fn to_error_record(&self) -> ErrorRecord {
548 let kind = match self {
549 ExecutionError::ValidationFailed(_) => "validation_failed",
550 ExecutionError::StepAborted(_) => "aborted",
551 ExecutionError::Io(_) => "io",
552 ExecutionError::StepFailed { .. } => "step_failed",
553 ExecutionError::UnknownAction(_) => "unknown_action",
554 ExecutionError::InvalidParams(_) => "invalid_params",
555 ExecutionError::MaterialLoadFailed { .. } => "material_load_failed",
556 ExecutionError::OutputWriteFailed { .. } => "output_write_failed",
557 ExecutionError::TranscriptError(_) => "transcript_error",
558 ExecutionError::BackendError(_) => "backend_error",
559 };
560 ErrorRecord::new(kind, self.to_string())
561 }
562}
563
564fn validate_parameters(ceremony: &Ceremony) -> Result<(), ExecutionError> {
565 let missing: Vec<_> = ceremony
566 .parameters
567 .iter()
568 .filter(|(_, p)| p.value.is_null())
569 .map(|(id, _)| id.as_str().to_string())
570 .collect();
571 if missing.is_empty() {
572 Ok(())
573 } else {
574 Err(ExecutionError::ValidationFailed(format!(
575 "Parameters missing values: {}",
576 missing.join(", ")
577 )))
578 }
579}
580
581fn merge_step_result(state: ExecutionState, result: StepResult) -> ExecutionState {
583 let mut artifacts = state.artifacts;
584 for (id, value) in result.artifacts {
585 artifacts.insert(id, value);
586 }
587 ExecutionState {
588 params: state.params,
589 roles: state.roles,
590 materials: state.materials,
591 dry_run: state.dry_run,
592 artifacts,
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use crossbeam_channel::unbounded;
599 use rite_model::ActionType;
600
601 use super::*;
602 use crate::actions::ActionCategory;
603 use crate::transcript_sink::InMemorySink;
604
605 struct PingAction;
606
607 impl Action for PingAction {
608 fn metadata(&self) -> ActionMetadata {
609 ActionMetadata {
610 action_type: ActionType::Attest,
611 description: "test action that always succeeds",
612 category: ActionCategory::Verification,
613 }
614 }
615
616 fn execute(
617 &self,
618 _step: &StepInfo,
619 _ctx: &HandlerContext,
620 _params: &serde_json::Value,
621 reporter: &mut Reporter<'_>,
622 _backend: Option<&mut dyn Backend>,
623 ) -> Result<StepResult, ActionError> {
624 reporter.log(Icon::Info, "ping")?;
625 Ok(StepResult::completed("pinged".to_string()))
626 }
627 }
628
629 fn minimal_ceremony() -> Ceremony {
630 let yaml = r#"
631version: "0.2"
632name: "Test"
633roles:
634 participant:
635 person: "Alice"
636sections:
637 main:
638 role: ${role.participant}
639 steps:
640 ping:
641 action: attest
642 silent: true
643 with:
644 statement: "I confirm."
645"#;
646 rite_resolver::resolve(yaml, None)
647 .into_result()
648 .expect("resolve")
649 }
650
651 fn dry_run_output_config() -> OutputConfig {
652 OutputConfig::new(std::path::PathBuf::from("/tmp/runner-test"))
653 }
654
655 #[test]
656 fn runs_a_minimal_ceremony_and_emits_lifecycle_facts() {
657 let ceremony = minimal_ceremony();
658 let mut registry = ActionRegistry::new();
659 registry.register(Arc::new(PingAction));
660
661 let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
662 let (event_tx, event_rx) = unbounded::<ExecEvent>();
663 let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
664
665 let executor = Executor::new(
666 ceremony,
667 registry,
668 BackendRegistry::new(),
669 dry_run_output_config(),
670 true,
671 StartupSnapshot::placeholder(),
672 );
673
674 let join = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
675
676 let mut facts = Vec::new();
678 while let Ok(ev) = event_rx.recv() {
679 if let ExecEvent::Fact(fact) = ev {
680 facts.push(fact);
681 }
682 }
683
684 let summary = join.join().expect("executor join").expect("run ok");
685 assert_eq!(summary.steps_completed, 1);
686
687 let kinds: Vec<&'static str> = facts.iter().map(fact_kind).collect();
688 assert_eq!(
689 kinds,
690 vec![
691 "ceremony_started",
692 "step_started",
693 "step_completed",
694 "ceremony_completed",
695 ],
696 );
697
698 drop(cmd_tx);
699 }
700
701 #[derive(Debug, PartialEq, Eq)]
702 enum PacingTrace {
703 CeremonyStarted,
704 StepStarted(String),
705 ContinuePrompt { step: Option<String> },
706 StepCompleted,
707 CeremonyCompleted,
708 }
709
710 #[test]
715 #[allow(clippy::too_many_lines)]
716 fn pacing_prompts_fire_at_step_start_not_step_end() {
717 let yaml = r#"
718version: "0.2"
719name: "Pacing"
720roles:
721 participant:
722 person: "Alice"
723sections:
724 main:
725 role: ${role.participant}
726 steps:
727 first:
728 action: attest
729 with:
730 statement: "first"
731 second:
732 action: attest
733 with:
734 statement: "second"
735"#;
736 let ceremony = rite_resolver::resolve(yaml, None)
737 .into_result()
738 .expect("resolve");
739 let mut registry = ActionRegistry::new();
740 registry.register(Arc::new(PingAction));
741
742 let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
743 let (event_tx, event_rx) = unbounded::<ExecEvent>();
744 let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
745
746 let executor = Executor::new(
747 ceremony,
748 registry,
749 BackendRegistry::new(),
750 dry_run_output_config(),
751 false, StartupSnapshot::placeholder(),
753 );
754
755 let frontend = std::thread::spawn({
759 let cmd_tx = cmd_tx.clone();
760 move || {
761 let mut trace = Vec::new();
762 while let Ok(event) = event_rx.recv() {
763 match event {
764 ExecEvent::Fact(StepFact::CeremonyStarted { .. }) => {
765 trace.push(PacingTrace::CeremonyStarted);
766 }
767 ExecEvent::Fact(StepFact::StepStarted { label, .. }) => {
768 trace.push(PacingTrace::StepStarted(label));
769 }
770 ExecEvent::Fact(StepFact::StepCompleted { .. }) => {
771 trace.push(PacingTrace::StepCompleted);
772 }
773 ExecEvent::Fact(StepFact::CeremonyCompleted { .. }) => {
774 trace.push(PacingTrace::CeremonyCompleted);
775 }
776 ExecEvent::AwaitPrompt {
777 prompt_id,
778 prompt: Prompt::Continue { .. },
779 step,
780 ..
781 } => {
782 trace.push(PacingTrace::ContinuePrompt {
783 step: step.map(|s| s.as_str().to_string()),
784 });
785 let _ = cmd_tx.send(UiCommand::PromptResponse {
786 prompt_id,
787 response: crate::protocol::Response::Acknowledge,
788 });
789 }
790 _ => {}
791 }
792 }
793 trace
794 }
795 });
796
797 let summary = executor
798 .run(&cmd_rx, &event_tx, sink)
799 .expect("ceremony runs");
800 assert_eq!(summary.steps_completed, 2);
801 drop(event_tx);
804 drop(cmd_tx);
805 let trace = frontend.join().expect("frontend join");
806
807 let mut iter = trace.iter();
813 assert_eq!(iter.next(), Some(&PacingTrace::CeremonyStarted));
814 assert_eq!(
815 iter.next(),
816 Some(&PacingTrace::ContinuePrompt { step: None })
817 );
818 let first_step = match iter.next() {
819 Some(PacingTrace::StepStarted(label)) => label.clone(),
820 other => panic!("expected first StepStarted, got {other:?}"),
821 };
822 assert_eq!(
823 iter.next(),
824 Some(&PacingTrace::ContinuePrompt {
825 step: Some("first".to_string()),
826 }),
827 "first step's Continue prompt must fire after StepStarted and before the body",
828 );
829 assert_eq!(iter.next(), Some(&PacingTrace::StepCompleted));
830 let second_step = match iter.next() {
831 Some(PacingTrace::StepStarted(label)) => label.clone(),
832 other => panic!("expected second StepStarted, got {other:?}"),
833 };
834 assert_eq!(
835 iter.next(),
836 Some(&PacingTrace::ContinuePrompt {
837 step: Some("second".to_string()),
838 }),
839 );
840 assert_eq!(iter.next(), Some(&PacingTrace::StepCompleted));
841 assert_eq!(iter.next(), Some(&PacingTrace::CeremonyCompleted));
842 assert_eq!(iter.next(), None, "no further events expected");
843 assert_eq!(first_step, "1");
845 assert_eq!(second_step, "2");
846 }
847
848 #[test]
849 fn aborts_when_action_returns_aborted() {
850 struct AbortAction;
851 impl Action for AbortAction {
852 fn metadata(&self) -> ActionMetadata {
853 ActionMetadata {
854 action_type: ActionType::Attest,
855 description: "aborts immediately",
856 category: ActionCategory::Verification,
857 }
858 }
859 fn execute(
860 &self,
861 _step: &StepInfo,
862 _ctx: &HandlerContext,
863 _params: &serde_json::Value,
864 _reporter: &mut Reporter<'_>,
865 _backend: Option<&mut dyn Backend>,
866 ) -> Result<StepResult, ActionError> {
867 Err(ActionError::Aborted)
868 }
869 }
870
871 let ceremony = minimal_ceremony();
872 let mut registry = ActionRegistry::new();
873 registry.register(Arc::new(AbortAction));
874
875 let (_cmd_tx, cmd_rx) = unbounded::<UiCommand>();
876 let (event_tx, event_rx) = unbounded::<ExecEvent>();
877 let sink: Box<dyn TranscriptSink> = Box::new(InMemorySink::new());
878
879 let executor = Executor::new(
880 ceremony,
881 registry,
882 BackendRegistry::new(),
883 dry_run_output_config(),
884 true,
885 StartupSnapshot::placeholder(),
886 );
887
888 let join = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
889
890 let mut got_failed = false;
891 while let Ok(ev) = event_rx.recv() {
892 if let ExecEvent::Fact(StepFact::CeremonyFailed { error, .. }) = ev {
893 assert_eq!(error.kind, "aborted");
894 got_failed = true;
895 }
896 }
897 assert!(got_failed, "expected CeremonyFailed event");
898
899 let result = join.join().expect("executor join");
900 assert!(matches!(result, Err(ExecutionError::StepAborted(_))));
901 }
902
903 fn fact_kind(fact: &StepFact) -> &'static str {
904 match fact {
905 StepFact::CeremonyStarted { .. } => "ceremony_started",
906 StepFact::ActStarted { .. } => "act_started",
907 StepFact::StepStarted { .. } => "step_started",
908 StepFact::PromptAnswered { .. } => "prompt_answered",
909 StepFact::BackendOperation { .. } => "backend_operation",
910 StepFact::AttestationRecorded { .. } => "attestation_recorded",
911 StepFact::ArtifactWritten { .. } => "artifact_written",
912 StepFact::DeviationRecorded { .. } => "deviation_recorded",
913 StepFact::StepCompleted { .. } => "step_completed",
914 StepFact::CeremonyCompleted { .. } => "ceremony_completed",
915 StepFact::CeremonyFailed { .. } => "ceremony_failed",
916 _ => "unknown",
917 }
918 }
919}