1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::DomainError;
12use crate::value_objects::{
13 CeremonyContext, CeremonyDefinitionDigest, CeremonyDefinitionDigestMigration,
14 CeremonyDescription, CeremonyGuard, CeremonyInputDefinition, CeremonyName,
15 CeremonyOutputDefinition, CeremonyRole, CeremonyState, CeremonyStep, CeremonyTransition,
16 CeremonyValidationReport, CeremonyVersion, GuardName, InputName, OutputName, RoleAction,
17 RoleId, StateId, StepExecutionRecord, StepId, TransitionTrigger,
18};
19
20use super::ceremony_definition_analysis::CeremonyDefinitionParts;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct CeremonyDefinition {
24 name: CeremonyName,
25 version: CeremonyVersion,
26 description: Option<CeremonyDescription>,
27 inputs: BTreeMap<InputName, CeremonyInputDefinition>,
28 outputs: BTreeMap<OutputName, CeremonyOutputDefinition>,
29 states: BTreeMap<StateId, CeremonyState>,
30 transitions: Vec<CeremonyTransition>,
31 steps: BTreeMap<StepId, CeremonyStep>,
32 step_order: Vec<StepId>,
33 guards: BTreeMap<GuardName, CeremonyGuard>,
34 roles: BTreeMap<RoleId, CeremonyRole>,
35}
36
37impl CeremonyDefinition {
38 pub fn new(
39 name: CeremonyName,
40 version: CeremonyVersion,
41 description: Option<CeremonyDescription>,
42 inputs: impl IntoIterator<Item = CeremonyInputDefinition>,
43 outputs: impl IntoIterator<Item = CeremonyOutputDefinition>,
44 states: impl IntoIterator<Item = CeremonyState>,
45 transitions: impl IntoIterator<Item = CeremonyTransition>,
46 steps: impl IntoIterator<Item = CeremonyStep>,
47 guards: impl IntoIterator<Item = CeremonyGuard>,
48 roles: impl IntoIterator<Item = CeremonyRole>,
49 ) -> Result<Self, DomainError> {
50 let inputs = collect_inputs(inputs)?;
51 let outputs = collect_outputs(outputs)?;
52 let states = collect_states(states)?;
53 let transitions = transitions.into_iter().collect::<Vec<_>>();
54 let (steps, step_order) = collect_steps(steps)?;
55 let guards = collect_guards(guards)?;
56 let roles = collect_roles(roles)?;
57
58 let definition = Self {
59 name,
60 version,
61 description,
62 inputs,
63 outputs,
64 states,
65 transitions,
66 steps,
67 step_order,
68 guards,
69 roles,
70 };
71 definition.validate()?;
72 Ok(definition)
73 }
74
75 #[must_use]
76 pub fn name(&self) -> &CeremonyName {
77 &self.name
78 }
79
80 #[must_use]
81 pub fn version(&self) -> &CeremonyVersion {
82 &self.version
83 }
84
85 #[must_use]
86 pub fn description(&self) -> Option<&CeremonyDescription> {
87 self.description.as_ref()
88 }
89
90 #[must_use]
91 pub fn inputs(&self) -> &BTreeMap<InputName, CeremonyInputDefinition> {
92 &self.inputs
93 }
94
95 #[must_use]
96 pub fn outputs(&self) -> &BTreeMap<OutputName, CeremonyOutputDefinition> {
97 &self.outputs
98 }
99
100 #[must_use]
101 pub fn states(&self) -> &BTreeMap<StateId, CeremonyState> {
102 &self.states
103 }
104
105 #[must_use]
106 pub fn transitions(&self) -> &[CeremonyTransition] {
107 &self.transitions
108 }
109
110 #[must_use]
111 pub fn steps(&self) -> &BTreeMap<StepId, CeremonyStep> {
112 &self.steps
113 }
114
115 pub fn steps_in_declaration_order(&self) -> impl Iterator<Item = &CeremonyStep> + '_ {
120 self.step_order.iter().map(|step_id| {
121 self.steps
122 .get(step_id)
123 .expect("ceremony step order must reference an indexed step")
124 })
125 }
126
127 #[must_use]
128 pub fn guards(&self) -> &BTreeMap<GuardName, CeremonyGuard> {
129 &self.guards
130 }
131
132 #[must_use]
133 pub fn roles(&self) -> &BTreeMap<RoleId, CeremonyRole> {
134 &self.roles
135 }
136
137 #[must_use]
138 pub fn initial_state_id(&self) -> &StateId {
139 self.states
140 .values()
141 .find(|state| state.is_initial())
142 .map(CeremonyState::id)
143 .expect("ceremony definition invariant requires one initial state")
144 }
145
146 #[must_use]
147 pub fn state(&self, state_id: &StateId) -> Option<&CeremonyState> {
148 self.states.get(state_id)
149 }
150
151 #[must_use]
152 pub fn step(&self, step_id: &StepId) -> Option<&CeremonyStep> {
153 self.steps.get(step_id)
154 }
155
156 #[must_use]
157 pub fn role(&self, role_id: &RoleId) -> Option<&CeremonyRole> {
158 self.roles.get(role_id)
159 }
160
161 pub fn steps_for_state(&self, state_id: &StateId) -> impl Iterator<Item = &CeremonyStep> + '_ {
162 let state_id = state_id.clone();
163 self.steps_in_declaration_order()
164 .filter(move |step| step.state_id() == &state_id)
165 }
166
167 #[must_use]
168 pub fn is_terminal_state(&self, state_id: &StateId) -> bool {
169 self.states
170 .get(state_id)
171 .is_some_and(CeremonyState::is_terminal)
172 }
173
174 #[must_use]
175 pub fn transition_for_trigger(
176 &self,
177 state_id: &StateId,
178 trigger: &TransitionTrigger,
179 ) -> Option<&CeremonyTransition> {
180 self.transitions
181 .iter()
182 .find(|transition| transition.from() == state_id && transition.trigger() == trigger)
183 }
184
185 pub fn available_transitions(
186 &self,
187 state_id: &StateId,
188 ) -> impl Iterator<Item = &CeremonyTransition> + '_ {
189 let state_id = state_id.clone();
190 self.transitions
191 .iter()
192 .filter(move |transition| transition.from() == &state_id)
193 }
194
195 #[must_use]
196 pub fn role_allows(&self, role_id: &RoleId, action: &RoleAction) -> bool {
197 self.roles
198 .get(role_id)
199 .is_some_and(|role| role.allows(action))
200 }
201
202 #[must_use]
203 pub fn guards_are_satisfied(
204 &self,
205 transition: &CeremonyTransition,
206 records: &BTreeMap<StepId, StepExecutionRecord>,
207 context: &CeremonyContext,
208 ) -> bool {
209 self.repeat_requirements_are_satisfied(transition.from(), records)
210 && transition.required_guards().iter().all(|guard_name| {
211 self.guards
212 .get(guard_name)
213 .is_some_and(|guard| self.guard_is_satisfied(guard, records, context))
214 })
215 }
216
217 #[must_use]
224 pub fn guard_is_satisfied(
225 &self,
226 guard: &CeremonyGuard,
227 records: &BTreeMap<StepId, StepExecutionRecord>,
228 context: &CeremonyContext,
229 ) -> bool {
230 if !guard.is_satisfied(records, context) {
231 return false;
232 }
233 match guard.condition() {
234 crate::value_objects::GuardCondition::StepStatus { step_id, status }
235 if status.is_success() =>
236 {
237 self.repeat_requirement_is_satisfied(step_id, records)
238 }
239 crate::value_objects::GuardCondition::AllStepsCompleted => self
240 .steps
241 .keys()
242 .all(|step_id| self.repeat_requirement_is_satisfied(step_id, records)),
243 _ => true,
244 }
245 }
246
247 #[must_use]
250 pub fn repeat_requirements_are_satisfied(
251 &self,
252 state_id: &StateId,
253 records: &BTreeMap<StepId, StepExecutionRecord>,
254 ) -> bool {
255 self.steps_for_state(state_id)
256 .all(|step| self.repeat_requirement_is_satisfied(step.id(), records))
257 }
258
259 fn repeat_requirement_is_satisfied(
260 &self,
261 step_id: &StepId,
262 records: &BTreeMap<StepId, StepExecutionRecord>,
263 ) -> bool {
264 let Some(policy) = self.step(step_id).and_then(CeremonyStep::repeat_policy) else {
265 return true;
266 };
267 records.get(step_id).is_some_and(|record| {
268 record.status().is_success() && policy.is_satisfied(record.output())
269 })
270 }
271
272 #[must_use]
278 pub fn role_for_action(&self, action: &RoleAction) -> Option<&CeremonyRole> {
279 self.roles.values().find(|role| role.allows(action))
280 }
281
282 pub fn role_id_for_step(&self, step_id: &StepId) -> Result<RoleId, DomainError> {
288 self.role_for_action(&RoleAction::step(step_id.clone()))
289 .map(|role| role.id().clone())
290 .ok_or(DomainError::InvariantViolated {
291 reason: "no ceremony role can execute step",
292 })
293 }
294
295 pub fn role_id_for_transition(
302 &self,
303 trigger: &TransitionTrigger,
304 ) -> Result<RoleId, DomainError> {
305 self.role_for_action(&RoleAction::transition(trigger.clone()))
306 .map(|role| role.id().clone())
307 .ok_or(DomainError::InvariantViolated {
308 reason: "no ceremony role can apply transition",
309 })
310 }
311
312 #[must_use]
321 pub fn next_satisfied_transition(
322 &self,
323 state_id: &StateId,
324 records: &BTreeMap<StepId, StepExecutionRecord>,
325 context: &CeremonyContext,
326 ) -> Option<&CeremonyTransition> {
327 self.available_transitions(state_id)
328 .find(|transition| self.guards_are_satisfied(transition, records, context))
329 }
330
331 pub fn digest(&self) -> Result<CeremonyDefinitionDigest, DomainError> {
349 let canonical = self.canonical_form()?;
350 Ok(CeremonyDefinitionDigest::of_canonical_form(&canonical))
351 }
352
353 pub fn choreographer_v1_digest_migration(
356 &self,
357 ) -> Result<CeremonyDefinitionDigestMigration, DomainError> {
358 let canonical = self.canonical_form()?;
359 Ok(CeremonyDefinitionDigestMigration::verified(
360 self.name.clone(),
361 self.version.clone(),
362 CeremonyDefinitionDigest::of_legacy_choreographer_canonical_form(&canonical),
363 CeremonyDefinitionDigest::of_canonical_form(&canonical),
364 ))
365 }
366
367 fn canonical_form(&self) -> Result<Vec<u8>, DomainError> {
368 serde_json::to_vec(self).map_err(|_| DomainError::InvariantViolated {
369 reason: "ceremony definition cannot be rendered canonically",
370 })
371 }
372
373 #[must_use]
383 pub fn analyze(&self) -> CeremonyValidationReport {
384 let mut findings = Vec::new();
385 self.parts().collect_findings(&mut findings);
386 CeremonyValidationReport::new(findings)
387 }
388
389 fn parts(&self) -> CeremonyDefinitionParts<'_> {
390 CeremonyDefinitionParts {
391 states: &self.states,
392 transitions: &self.transitions,
393 steps: &self.steps,
394 guards: &self.guards,
395 roles: &self.roles,
396 }
397 }
398
399 fn validate(&self) -> Result<(), DomainError> {
400 match self.analyze().first_error() {
401 Some(finding) => Err(finding.defect().clone()),
402 None => Ok(()),
403 }
404 }
405}
406
407fn collect_inputs(
408 inputs: impl IntoIterator<Item = CeremonyInputDefinition>,
409) -> Result<BTreeMap<InputName, CeremonyInputDefinition>, DomainError> {
410 let mut map = BTreeMap::new();
411 for input in inputs {
412 if map.insert(input.name().clone(), input).is_some() {
413 return Err(DomainError::AlreadyExists {
414 what: "ceremony_input",
415 });
416 }
417 }
418 Ok(map)
419}
420
421fn collect_outputs(
422 outputs: impl IntoIterator<Item = CeremonyOutputDefinition>,
423) -> Result<BTreeMap<OutputName, CeremonyOutputDefinition>, DomainError> {
424 let mut map = BTreeMap::new();
425 for output in outputs {
426 if map.insert(output.name().clone(), output).is_some() {
427 return Err(DomainError::AlreadyExists {
428 what: "ceremony_output",
429 });
430 }
431 }
432 Ok(map)
433}
434
435fn collect_states(
436 states: impl IntoIterator<Item = CeremonyState>,
437) -> Result<BTreeMap<StateId, CeremonyState>, DomainError> {
438 let mut map = BTreeMap::new();
439 for state in states {
440 if map.insert(state.id().clone(), state).is_some() {
441 return Err(DomainError::AlreadyExists {
442 what: "ceremony_state",
443 });
444 }
445 }
446 Ok(map)
447}
448
449fn collect_steps(
450 steps: impl IntoIterator<Item = CeremonyStep>,
451) -> Result<(BTreeMap<StepId, CeremonyStep>, Vec<StepId>), DomainError> {
452 let mut map = BTreeMap::new();
453 let mut order = Vec::new();
454 for step in steps {
455 let step_id = step.id().clone();
456 if map.insert(step_id.clone(), step).is_some() {
457 return Err(DomainError::AlreadyExists {
458 what: "ceremony_step",
459 });
460 }
461 order.push(step_id);
462 }
463 Ok((map, order))
464}
465
466fn collect_guards(
467 guards: impl IntoIterator<Item = CeremonyGuard>,
468) -> Result<BTreeMap<GuardName, CeremonyGuard>, DomainError> {
469 let mut map = BTreeMap::new();
470 for guard in guards {
471 if map.insert(guard.name().clone(), guard).is_some() {
472 return Err(DomainError::AlreadyExists {
473 what: "ceremony_guard",
474 });
475 }
476 }
477 Ok(map)
478}
479
480fn collect_roles(
481 roles: impl IntoIterator<Item = CeremonyRole>,
482) -> Result<BTreeMap<RoleId, CeremonyRole>, DomainError> {
483 let mut map = BTreeMap::new();
484 for role in roles {
485 if map.insert(role.id().clone(), role).is_some() {
486 return Err(DomainError::AlreadyExists {
487 what: "ceremony_role",
488 });
489 }
490 }
491 Ok(map)
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::value_objects::{
498 CeremonyStateKind, CeremonyValidationLocus, GuardCondition, RetryPolicy, StepHandlerConfig,
499 StepHandlerKind, StepStatus,
500 };
501
502 fn name() -> CeremonyName {
503 CeremonyName::new("planning_ceremony").unwrap()
504 }
505
506 fn state_id(raw: &str) -> StateId {
507 StateId::new(raw).unwrap()
508 }
509
510 fn step_id(raw: &str) -> StepId {
511 StepId::new(raw).unwrap()
512 }
513
514 fn guard_name(raw: &str) -> GuardName {
515 GuardName::new(raw).unwrap()
516 }
517
518 fn trigger(raw: &str) -> TransitionTrigger {
519 TransitionTrigger::new(raw).unwrap()
520 }
521
522 fn handler_kind() -> StepHandlerKind {
523 StepHandlerKind::new("manual_review").unwrap()
524 }
525
526 fn step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
527 CeremonyStep::new(
528 step_id(raw_step_id),
529 state_id(raw_state_id),
530 handler_kind(),
531 StepHandlerConfig::empty(),
532 RetryPolicy::single_attempt(),
533 None,
534 )
535 }
536
537 fn role(actions: Vec<RoleAction>) -> CeremonyRole {
538 CeremonyRole::new(RoleId::new("facilitator").unwrap(), actions).unwrap()
539 }
540
541 fn definition(
542 states: Vec<CeremonyState>,
543 transitions: Vec<CeremonyTransition>,
544 steps: Vec<CeremonyStep>,
545 guards: Vec<CeremonyGuard>,
546 roles: Vec<CeremonyRole>,
547 ) -> Result<CeremonyDefinition, DomainError> {
548 CeremonyDefinition::new(
549 name(),
550 CeremonyVersion::v1(),
551 None,
552 Vec::new(),
553 Vec::new(),
554 states,
555 transitions,
556 steps,
557 guards,
558 roles,
559 )
560 }
561
562 fn valid_definition() -> CeremonyDefinition {
563 let plan_step = step("plan", "drafting");
564 let guard = CeremonyGuard::new(
565 guard_name("plan_done"),
566 GuardCondition::StepStatus {
567 step_id: plan_step.id().clone(),
568 status: StepStatus::Completed,
569 },
570 );
571 let transition = CeremonyTransition::new(
572 state_id("drafting"),
573 state_id("done"),
574 trigger("finish"),
575 vec![guard.name().clone()],
576 )
577 .unwrap();
578 let role = role(vec![
579 RoleAction::step(plan_step.id().clone()),
580 RoleAction::transition(transition.trigger().clone()),
581 ]);
582
583 definition(
584 vec![
585 CeremonyState::initial(state_id("drafting")),
586 CeremonyState::terminal(state_id("done")),
587 ],
588 vec![transition],
589 vec![plan_step],
590 vec![guard],
591 vec![role],
592 )
593 .unwrap()
594 }
595
596 #[test]
597 fn accepts_valid_declarative_state_machine() {
598 let definition = valid_definition();
599
600 assert_eq!(definition.initial_state_id(), &state_id("drafting"));
601 assert_eq!(definition.steps_for_state(&state_id("drafting")).count(), 1);
602 assert!(definition.role_allows(
603 &RoleId::new("facilitator").unwrap(),
604 &RoleAction::transition(trigger("finish"))
605 ));
606 }
607
608 #[test]
609 fn preserves_step_declaration_order_within_a_state() {
610 let definition = definition(
611 vec![
612 CeremonyState::initial(state_id("drafting")),
613 CeremonyState::terminal(state_id("done")),
614 ],
615 Vec::new(),
616 vec![
617 step("write_plan", "drafting"),
618 step("challenge_plan", "drafting"),
619 step("archive_plan", "drafting"),
620 ],
621 Vec::new(),
622 Vec::new(),
623 )
624 .unwrap();
625
626 let step_ids = definition
627 .steps_for_state(&state_id("drafting"))
628 .map(CeremonyStep::id)
629 .cloned()
630 .collect::<Vec<_>>();
631
632 assert_eq!(
633 step_ids,
634 vec![
635 step_id("write_plan"),
636 step_id("challenge_plan"),
637 step_id("archive_plan"),
638 ]
639 );
640 }
641
642 #[test]
643 fn rejects_definitions_without_exactly_one_initial_state() {
644 let err = definition(
645 vec![
646 CeremonyState::new(state_id("one"), CeremonyStateKind::Initial),
647 CeremonyState::new(state_id("two"), CeremonyStateKind::Initial),
648 CeremonyState::terminal(state_id("done")),
649 ],
650 Vec::new(),
651 Vec::new(),
652 Vec::new(),
653 Vec::new(),
654 )
655 .unwrap_err();
656
657 assert!(matches!(err, DomainError::InvariantViolated { .. }));
658 }
659
660 #[test]
661 fn rejects_terminal_states_with_outgoing_transitions() {
662 let transition = CeremonyTransition::new(
663 state_id("done"),
664 state_id("drafting"),
665 trigger("restart"),
666 Vec::new(),
667 )
668 .unwrap();
669
670 let err = definition(
671 vec![
672 CeremonyState::initial(state_id("drafting")),
673 CeremonyState::terminal(state_id("done")),
674 ],
675 vec![transition],
676 Vec::new(),
677 Vec::new(),
678 Vec::new(),
679 )
680 .unwrap_err();
681
682 assert!(matches!(err, DomainError::InvariantViolated { .. }));
683 }
684
685 #[test]
686 fn rejects_roles_that_reference_unknown_steps() {
687 let role = role(vec![RoleAction::step(step_id("missing"))]);
688
689 let err = definition(
690 vec![
691 CeremonyState::initial(state_id("drafting")),
692 CeremonyState::terminal(state_id("done")),
693 ],
694 Vec::new(),
695 Vec::new(),
696 Vec::new(),
697 vec![role],
698 )
699 .unwrap_err();
700
701 assert!(matches!(
702 err,
703 DomainError::NotFound {
704 what: "ceremony_role.step_action"
705 }
706 ));
707 }
708
709 #[test]
710 fn rejects_empty_states_collection() {
711 let err =
712 definition(Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()).unwrap_err();
713
714 assert!(matches!(
715 err,
716 DomainError::EmptyCollection {
717 field: "ceremony_definition.states"
718 }
719 ));
720 }
721
722 #[test]
723 fn rejects_transition_referencing_unknown_from_state() {
724 let transition = CeremonyTransition::new(
725 state_id("ghost"),
726 state_id("done"),
727 trigger("finish"),
728 Vec::new(),
729 )
730 .unwrap();
731
732 let err = definition(
733 vec![
734 CeremonyState::initial(state_id("drafting")),
735 CeremonyState::terminal(state_id("done")),
736 ],
737 vec![transition],
738 Vec::new(),
739 Vec::new(),
740 Vec::new(),
741 )
742 .unwrap_err();
743
744 assert!(matches!(
745 err,
746 DomainError::NotFound {
747 what: "ceremony_transition.from_state"
748 }
749 ));
750 }
751
752 #[test]
753 fn rejects_transition_referencing_unknown_to_state() {
754 let transition = CeremonyTransition::new(
755 state_id("drafting"),
756 state_id("ghost"),
757 trigger("finish"),
758 Vec::new(),
759 )
760 .unwrap();
761
762 let err = definition(
763 vec![
764 CeremonyState::initial(state_id("drafting")),
765 CeremonyState::terminal(state_id("done")),
766 ],
767 vec![transition],
768 Vec::new(),
769 Vec::new(),
770 Vec::new(),
771 )
772 .unwrap_err();
773
774 assert!(matches!(
775 err,
776 DomainError::NotFound {
777 what: "ceremony_transition.to_state"
778 }
779 ));
780 }
781
782 #[test]
783 fn rejects_duplicate_state_trigger_pairs() {
784 let first = CeremonyTransition::new(
785 state_id("drafting"),
786 state_id("done"),
787 trigger("finish"),
788 Vec::new(),
789 )
790 .unwrap();
791 let duplicate = CeremonyTransition::new(
792 state_id("drafting"),
793 state_id("done"),
794 trigger("finish"),
795 Vec::new(),
796 )
797 .unwrap();
798
799 let err = definition(
800 vec![
801 CeremonyState::initial(state_id("drafting")),
802 CeremonyState::terminal(state_id("done")),
803 ],
804 vec![first, duplicate],
805 Vec::new(),
806 Vec::new(),
807 Vec::new(),
808 )
809 .unwrap_err();
810
811 assert!(matches!(
812 err,
813 DomainError::AlreadyExists {
814 what: "ceremony_transition.state_trigger"
815 }
816 ));
817 }
818
819 #[test]
820 fn rejects_transition_referencing_unknown_guard() {
821 let transition = CeremonyTransition::new(
822 state_id("drafting"),
823 state_id("done"),
824 trigger("finish"),
825 vec![guard_name("absent")],
826 )
827 .unwrap();
828
829 let err = definition(
830 vec![
831 CeremonyState::initial(state_id("drafting")),
832 CeremonyState::terminal(state_id("done")),
833 ],
834 vec![transition],
835 Vec::new(),
836 Vec::new(),
837 Vec::new(),
838 )
839 .unwrap_err();
840
841 assert!(matches!(
842 err,
843 DomainError::NotFound {
844 what: "ceremony_transition.guard"
845 }
846 ));
847 }
848
849 #[test]
850 fn rejects_step_referencing_unknown_state() {
851 let err = definition(
852 vec![
853 CeremonyState::initial(state_id("drafting")),
854 CeremonyState::terminal(state_id("done")),
855 ],
856 Vec::new(),
857 vec![step("plan", "ghost")],
858 Vec::new(),
859 Vec::new(),
860 )
861 .unwrap_err();
862
863 assert!(matches!(
864 err,
865 DomainError::NotFound {
866 what: "ceremony_step.state"
867 }
868 ));
869 }
870
871 #[test]
872 fn rejects_role_that_references_unknown_transition_trigger() {
873 let role = role(vec![RoleAction::transition(trigger("ghost"))]);
874
875 let err = definition(
876 vec![
877 CeremonyState::initial(state_id("drafting")),
878 CeremonyState::terminal(state_id("done")),
879 ],
880 Vec::new(),
881 Vec::new(),
882 Vec::new(),
883 vec![role],
884 )
885 .unwrap_err();
886
887 assert!(matches!(
888 err,
889 DomainError::NotFound {
890 what: "ceremony_role.transition_action"
891 }
892 ));
893 }
894
895 #[test]
896 fn resolves_role_authorised_for_a_step() {
897 let definition = valid_definition();
898
899 assert_eq!(
900 definition.role_id_for_step(&step_id("plan")).unwrap(),
901 RoleId::new("facilitator").unwrap()
902 );
903 }
904
905 #[test]
906 fn rejects_step_with_no_authorised_role() {
907 let definition = valid_definition();
908
909 let err = definition
910 .role_id_for_step(&step_id("unowned"))
911 .unwrap_err();
912
913 assert!(matches!(
914 err,
915 DomainError::InvariantViolated {
916 reason: "no ceremony role can execute step"
917 }
918 ));
919 }
920
921 #[test]
922 fn resolves_role_authorised_for_a_transition() {
923 let definition = valid_definition();
924
925 assert_eq!(
926 definition
927 .role_id_for_transition(&trigger("finish"))
928 .unwrap(),
929 RoleId::new("facilitator").unwrap()
930 );
931 }
932
933 #[test]
934 fn rejects_transition_with_no_authorised_role() {
935 let definition = valid_definition();
936
937 let err = definition
938 .role_id_for_transition(&trigger("unowned"))
939 .unwrap_err();
940
941 assert!(matches!(
942 err,
943 DomainError::InvariantViolated {
944 reason: "no ceremony role can apply transition"
945 }
946 ));
947 }
948
949 #[test]
950 fn selects_guardless_transition_as_immediately_enabled() {
951 let transition = CeremonyTransition::new(
952 state_id("open"),
953 state_id("closed"),
954 trigger("go"),
955 Vec::new(),
956 )
957 .unwrap();
958 let definition = definition(
959 vec![
960 CeremonyState::initial(state_id("open")),
961 CeremonyState::terminal(state_id("closed")),
962 ],
963 vec![transition],
964 Vec::new(),
965 Vec::new(),
966 Vec::new(),
967 )
968 .unwrap();
969
970 let selected = definition
971 .next_satisfied_transition(
972 &state_id("open"),
973 &BTreeMap::new(),
974 &CeremonyContext::empty(),
975 )
976 .expect("guardless transition is always enabled");
977
978 assert_eq!(selected.trigger(), &trigger("go"));
979 }
980
981 #[test]
982 fn skips_transition_whose_guards_are_unsatisfied() {
983 let definition = valid_definition();
984
985 assert!(definition
986 .next_satisfied_transition(
987 &state_id("drafting"),
988 &BTreeMap::new(),
989 &CeremonyContext::empty(),
990 )
991 .is_none());
992 }
993
994 #[test]
995 fn yields_no_transition_out_of_a_terminal_state() {
996 let definition = valid_definition();
997
998 assert!(definition
999 .next_satisfied_transition(
1000 &state_id("done"),
1001 &BTreeMap::new(),
1002 &CeremonyContext::empty(),
1003 )
1004 .is_none());
1005 }
1006
1007 #[test]
1008 fn rejects_guards_that_reference_unknown_steps() {
1009 let guard = CeremonyGuard::new(
1010 guard_name("unknown_step_done"),
1011 GuardCondition::StepStatus {
1012 step_id: step_id("missing"),
1013 status: StepStatus::Completed,
1014 },
1015 );
1016
1017 let err = definition(
1018 vec![
1019 CeremonyState::initial(state_id("drafting")),
1020 CeremonyState::terminal(state_id("done")),
1021 ],
1022 Vec::new(),
1023 Vec::new(),
1024 vec![guard],
1025 Vec::new(),
1026 )
1027 .unwrap_err();
1028
1029 assert!(matches!(
1030 err,
1031 DomainError::NotFound {
1032 what: "ceremony_guard.step"
1033 }
1034 ));
1035 }
1036
1037 #[test]
1043 fn serde_json_emits_sorted_keys() {
1044 let mut out_of_order = serde_json::Map::new();
1045 out_of_order.insert("zulu".to_owned(), serde_json::Value::from(1));
1046 out_of_order.insert("alpha".to_owned(), serde_json::Value::from(2));
1047
1048 assert_eq!(
1049 serde_json::to_string(&serde_json::Value::Object(out_of_order)).unwrap(),
1050 r#"{"alpha":2,"zulu":1}"#,
1051 "serde_json is no longer emitting sorted keys, so the definition digest is not canonical"
1052 );
1053 }
1054
1055 #[test]
1056 fn the_same_definition_always_digests_the_same() {
1057 assert_eq!(
1058 valid_definition().digest().unwrap(),
1059 valid_definition().digest().unwrap()
1060 );
1061 }
1062
1063 #[test]
1064 fn rename_migration_keeps_content_but_changes_the_digest_scheme() {
1065 let definition = valid_definition();
1066 let migration = definition.choreographer_v1_digest_migration().unwrap();
1067
1068 assert_eq!(migration.definition_name(), definition.name());
1069 assert_eq!(migration.definition_version(), definition.version());
1070 assert_eq!(migration.destination(), definition.digest().unwrap());
1071 assert_ne!(migration.source(), migration.destination());
1072 }
1073
1074 #[test]
1075 fn a_material_difference_changes_the_digest() {
1076 let baseline = valid_definition().digest().unwrap();
1077 let renamed = definition(
1078 vec![
1079 CeremonyState::initial(state_id("drafting")),
1080 CeremonyState::terminal(state_id("finished")),
1081 ],
1082 vec![CeremonyTransition::new(
1083 state_id("drafting"),
1084 state_id("finished"),
1085 trigger("finish"),
1086 Vec::new(),
1087 )
1088 .unwrap()],
1089 Vec::new(),
1090 Vec::new(),
1091 Vec::new(),
1092 )
1093 .unwrap()
1094 .digest()
1095 .unwrap();
1096
1097 assert_ne!(baseline, renamed);
1098 }
1099
1100 #[test]
1101 fn transition_order_is_material_to_the_digest() {
1102 let states = || {
1106 vec![
1107 CeremonyState::initial(state_id("drafting")),
1108 CeremonyState::terminal(state_id("done")),
1109 CeremonyState::terminal(state_id("cancelled")),
1110 ]
1111 };
1112 let finish = || {
1113 CeremonyTransition::new(
1114 state_id("drafting"),
1115 state_id("done"),
1116 trigger("finish"),
1117 Vec::new(),
1118 )
1119 .unwrap()
1120 };
1121 let cancel = || {
1122 CeremonyTransition::new(
1123 state_id("drafting"),
1124 state_id("cancelled"),
1125 trigger("cancel"),
1126 Vec::new(),
1127 )
1128 .unwrap()
1129 };
1130
1131 let first = definition(
1132 states(),
1133 vec![finish(), cancel()],
1134 Vec::new(),
1135 Vec::new(),
1136 Vec::new(),
1137 )
1138 .unwrap();
1139 let swapped = definition(
1140 states(),
1141 vec![cancel(), finish()],
1142 Vec::new(),
1143 Vec::new(),
1144 Vec::new(),
1145 )
1146 .unwrap();
1147
1148 assert_ne!(first.digest().unwrap(), swapped.digest().unwrap());
1149 }
1150
1151 #[test]
1152 fn a_valid_definition_reports_no_findings_at_all() {
1153 let report = valid_definition().analyze();
1154
1155 assert!(report.is_valid());
1156 assert!(report.findings().is_empty());
1157 }
1158
1159 #[test]
1160 fn an_unreachable_state_is_warned_about_without_blocking_construction() {
1161 let definition = definition(
1162 vec![
1163 CeremonyState::initial(state_id("drafting")),
1164 CeremonyState::intermediate(state_id("orphan")),
1165 CeremonyState::terminal(state_id("done")),
1166 ],
1167 vec![
1168 CeremonyTransition::new(
1169 state_id("drafting"),
1170 state_id("done"),
1171 trigger("finish"),
1172 Vec::new(),
1173 )
1174 .unwrap(),
1175 CeremonyTransition::new(
1176 state_id("orphan"),
1177 state_id("done"),
1178 trigger("rescue"),
1179 Vec::new(),
1180 )
1181 .unwrap(),
1182 ],
1183 Vec::new(),
1184 Vec::new(),
1185 Vec::new(),
1186 )
1187 .unwrap();
1188
1189 let report = definition.analyze();
1190 let warnings = report.warnings().collect::<Vec<_>>();
1191
1192 assert!(report.is_valid());
1193 assert_eq!(warnings.len(), 1);
1194 assert_eq!(
1195 warnings[0].locus(),
1196 &CeremonyValidationLocus::state(state_id("orphan"))
1197 );
1198 }
1199
1200 #[test]
1201 fn a_state_that_cannot_reach_a_terminal_is_warned_about() {
1202 let definition = definition(
1203 vec![
1204 CeremonyState::initial(state_id("drafting")),
1205 CeremonyState::intermediate(state_id("stuck")),
1206 CeremonyState::terminal(state_id("done")),
1207 ],
1208 vec![
1209 CeremonyTransition::new(
1210 state_id("drafting"),
1211 state_id("done"),
1212 trigger("finish"),
1213 Vec::new(),
1214 )
1215 .unwrap(),
1216 CeremonyTransition::new(
1217 state_id("drafting"),
1218 state_id("stuck"),
1219 trigger("stall"),
1220 Vec::new(),
1221 )
1222 .unwrap(),
1223 CeremonyTransition::new(
1224 state_id("stuck"),
1225 state_id("stuck"),
1226 trigger("spin"),
1227 Vec::new(),
1228 )
1229 .unwrap(),
1230 ],
1231 Vec::new(),
1232 Vec::new(),
1233 Vec::new(),
1234 )
1235 .unwrap();
1236
1237 let report = definition.analyze();
1238 let warnings = report.warnings().collect::<Vec<_>>();
1239
1240 assert_eq!(warnings.len(), 1);
1241 assert_eq!(
1242 warnings[0].locus(),
1243 &CeremonyValidationLocus::state(state_id("stuck"))
1244 );
1245 }
1246
1247 #[test]
1248 fn a_definition_without_any_terminal_state_is_warned_about() {
1249 let definition = definition(
1250 vec![CeremonyState::initial(state_id("drafting"))],
1251 Vec::new(),
1252 Vec::new(),
1253 Vec::new(),
1254 Vec::new(),
1255 )
1256 .unwrap();
1257
1258 let report = definition.analyze();
1259 let warnings = report.warnings().collect::<Vec<_>>();
1260
1261 assert_eq!(warnings.len(), 1);
1262 assert_eq!(warnings[0].locus(), &CeremonyValidationLocus::Definition);
1263 }
1264
1265 #[test]
1266 fn structural_errors_suppress_reachability_noise() {
1267 let report = definition(
1268 vec![
1269 CeremonyState::initial(state_id("drafting")),
1270 CeremonyState::initial(state_id("also_drafting")),
1271 CeremonyState::terminal(state_id("done")),
1272 ],
1273 Vec::new(),
1274 Vec::new(),
1275 Vec::new(),
1276 Vec::new(),
1277 )
1278 .unwrap_err();
1279
1280 assert!(matches!(
1281 report,
1282 DomainError::InvariantViolated {
1283 reason: "ceremony definition must have exactly one initial state"
1284 }
1285 ));
1286 }
1287}