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 transition.required_guards().iter().all(|guard_name| {
210 self.guards
211 .get(guard_name)
212 .is_some_and(|guard| guard.is_satisfied(records, context))
213 })
214 }
215
216 #[must_use]
222 pub fn role_for_action(&self, action: &RoleAction) -> Option<&CeremonyRole> {
223 self.roles.values().find(|role| role.allows(action))
224 }
225
226 pub fn role_id_for_step(&self, step_id: &StepId) -> Result<RoleId, DomainError> {
232 self.role_for_action(&RoleAction::step(step_id.clone()))
233 .map(|role| role.id().clone())
234 .ok_or(DomainError::InvariantViolated {
235 reason: "no ceremony role can execute step",
236 })
237 }
238
239 pub fn role_id_for_transition(
246 &self,
247 trigger: &TransitionTrigger,
248 ) -> Result<RoleId, DomainError> {
249 self.role_for_action(&RoleAction::transition(trigger.clone()))
250 .map(|role| role.id().clone())
251 .ok_or(DomainError::InvariantViolated {
252 reason: "no ceremony role can apply transition",
253 })
254 }
255
256 #[must_use]
265 pub fn next_satisfied_transition(
266 &self,
267 state_id: &StateId,
268 records: &BTreeMap<StepId, StepExecutionRecord>,
269 context: &CeremonyContext,
270 ) -> Option<&CeremonyTransition> {
271 self.available_transitions(state_id)
272 .find(|transition| self.guards_are_satisfied(transition, records, context))
273 }
274
275 pub fn digest(&self) -> Result<CeremonyDefinitionDigest, DomainError> {
293 let canonical = self.canonical_form()?;
294 Ok(CeremonyDefinitionDigest::of_canonical_form(&canonical))
295 }
296
297 pub fn choreographer_v1_digest_migration(
300 &self,
301 ) -> Result<CeremonyDefinitionDigestMigration, DomainError> {
302 let canonical = self.canonical_form()?;
303 Ok(CeremonyDefinitionDigestMigration::verified(
304 self.name.clone(),
305 self.version.clone(),
306 CeremonyDefinitionDigest::of_legacy_choreographer_canonical_form(&canonical),
307 CeremonyDefinitionDigest::of_canonical_form(&canonical),
308 ))
309 }
310
311 fn canonical_form(&self) -> Result<Vec<u8>, DomainError> {
312 serde_json::to_vec(self).map_err(|_| DomainError::InvariantViolated {
313 reason: "ceremony definition cannot be rendered canonically",
314 })
315 }
316
317 #[must_use]
327 pub fn analyze(&self) -> CeremonyValidationReport {
328 let mut findings = Vec::new();
329 self.parts().collect_findings(&mut findings);
330 CeremonyValidationReport::new(findings)
331 }
332
333 fn parts(&self) -> CeremonyDefinitionParts<'_> {
334 CeremonyDefinitionParts {
335 states: &self.states,
336 transitions: &self.transitions,
337 steps: &self.steps,
338 guards: &self.guards,
339 roles: &self.roles,
340 }
341 }
342
343 fn validate(&self) -> Result<(), DomainError> {
344 match self.analyze().first_error() {
345 Some(finding) => Err(finding.defect().clone()),
346 None => Ok(()),
347 }
348 }
349}
350
351fn collect_inputs(
352 inputs: impl IntoIterator<Item = CeremonyInputDefinition>,
353) -> Result<BTreeMap<InputName, CeremonyInputDefinition>, DomainError> {
354 let mut map = BTreeMap::new();
355 for input in inputs {
356 if map.insert(input.name().clone(), input).is_some() {
357 return Err(DomainError::AlreadyExists {
358 what: "ceremony_input",
359 });
360 }
361 }
362 Ok(map)
363}
364
365fn collect_outputs(
366 outputs: impl IntoIterator<Item = CeremonyOutputDefinition>,
367) -> Result<BTreeMap<OutputName, CeremonyOutputDefinition>, DomainError> {
368 let mut map = BTreeMap::new();
369 for output in outputs {
370 if map.insert(output.name().clone(), output).is_some() {
371 return Err(DomainError::AlreadyExists {
372 what: "ceremony_output",
373 });
374 }
375 }
376 Ok(map)
377}
378
379fn collect_states(
380 states: impl IntoIterator<Item = CeremonyState>,
381) -> Result<BTreeMap<StateId, CeremonyState>, DomainError> {
382 let mut map = BTreeMap::new();
383 for state in states {
384 if map.insert(state.id().clone(), state).is_some() {
385 return Err(DomainError::AlreadyExists {
386 what: "ceremony_state",
387 });
388 }
389 }
390 Ok(map)
391}
392
393fn collect_steps(
394 steps: impl IntoIterator<Item = CeremonyStep>,
395) -> Result<(BTreeMap<StepId, CeremonyStep>, Vec<StepId>), DomainError> {
396 let mut map = BTreeMap::new();
397 let mut order = Vec::new();
398 for step in steps {
399 let step_id = step.id().clone();
400 if map.insert(step_id.clone(), step).is_some() {
401 return Err(DomainError::AlreadyExists {
402 what: "ceremony_step",
403 });
404 }
405 order.push(step_id);
406 }
407 Ok((map, order))
408}
409
410fn collect_guards(
411 guards: impl IntoIterator<Item = CeremonyGuard>,
412) -> Result<BTreeMap<GuardName, CeremonyGuard>, DomainError> {
413 let mut map = BTreeMap::new();
414 for guard in guards {
415 if map.insert(guard.name().clone(), guard).is_some() {
416 return Err(DomainError::AlreadyExists {
417 what: "ceremony_guard",
418 });
419 }
420 }
421 Ok(map)
422}
423
424fn collect_roles(
425 roles: impl IntoIterator<Item = CeremonyRole>,
426) -> Result<BTreeMap<RoleId, CeremonyRole>, DomainError> {
427 let mut map = BTreeMap::new();
428 for role in roles {
429 if map.insert(role.id().clone(), role).is_some() {
430 return Err(DomainError::AlreadyExists {
431 what: "ceremony_role",
432 });
433 }
434 }
435 Ok(map)
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::value_objects::{
442 CeremonyStateKind, CeremonyValidationLocus, GuardCondition, RetryPolicy, StepHandlerConfig,
443 StepHandlerKind, StepStatus,
444 };
445
446 fn name() -> CeremonyName {
447 CeremonyName::new("planning_ceremony").unwrap()
448 }
449
450 fn state_id(raw: &str) -> StateId {
451 StateId::new(raw).unwrap()
452 }
453
454 fn step_id(raw: &str) -> StepId {
455 StepId::new(raw).unwrap()
456 }
457
458 fn guard_name(raw: &str) -> GuardName {
459 GuardName::new(raw).unwrap()
460 }
461
462 fn trigger(raw: &str) -> TransitionTrigger {
463 TransitionTrigger::new(raw).unwrap()
464 }
465
466 fn handler_kind() -> StepHandlerKind {
467 StepHandlerKind::new("manual_review").unwrap()
468 }
469
470 fn step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
471 CeremonyStep::new(
472 step_id(raw_step_id),
473 state_id(raw_state_id),
474 handler_kind(),
475 StepHandlerConfig::empty(),
476 RetryPolicy::single_attempt(),
477 None,
478 )
479 }
480
481 fn role(actions: Vec<RoleAction>) -> CeremonyRole {
482 CeremonyRole::new(RoleId::new("facilitator").unwrap(), actions).unwrap()
483 }
484
485 fn definition(
486 states: Vec<CeremonyState>,
487 transitions: Vec<CeremonyTransition>,
488 steps: Vec<CeremonyStep>,
489 guards: Vec<CeremonyGuard>,
490 roles: Vec<CeremonyRole>,
491 ) -> Result<CeremonyDefinition, DomainError> {
492 CeremonyDefinition::new(
493 name(),
494 CeremonyVersion::v1(),
495 None,
496 Vec::new(),
497 Vec::new(),
498 states,
499 transitions,
500 steps,
501 guards,
502 roles,
503 )
504 }
505
506 fn valid_definition() -> CeremonyDefinition {
507 let plan_step = step("plan", "drafting");
508 let guard = CeremonyGuard::new(
509 guard_name("plan_done"),
510 GuardCondition::StepStatus {
511 step_id: plan_step.id().clone(),
512 status: StepStatus::Completed,
513 },
514 );
515 let transition = CeremonyTransition::new(
516 state_id("drafting"),
517 state_id("done"),
518 trigger("finish"),
519 vec![guard.name().clone()],
520 )
521 .unwrap();
522 let role = role(vec![
523 RoleAction::step(plan_step.id().clone()),
524 RoleAction::transition(transition.trigger().clone()),
525 ]);
526
527 definition(
528 vec![
529 CeremonyState::initial(state_id("drafting")),
530 CeremonyState::terminal(state_id("done")),
531 ],
532 vec![transition],
533 vec![plan_step],
534 vec![guard],
535 vec![role],
536 )
537 .unwrap()
538 }
539
540 #[test]
541 fn accepts_valid_declarative_state_machine() {
542 let definition = valid_definition();
543
544 assert_eq!(definition.initial_state_id(), &state_id("drafting"));
545 assert_eq!(definition.steps_for_state(&state_id("drafting")).count(), 1);
546 assert!(definition.role_allows(
547 &RoleId::new("facilitator").unwrap(),
548 &RoleAction::transition(trigger("finish"))
549 ));
550 }
551
552 #[test]
553 fn preserves_step_declaration_order_within_a_state() {
554 let definition = definition(
555 vec![
556 CeremonyState::initial(state_id("drafting")),
557 CeremonyState::terminal(state_id("done")),
558 ],
559 Vec::new(),
560 vec![
561 step("write_plan", "drafting"),
562 step("challenge_plan", "drafting"),
563 step("archive_plan", "drafting"),
564 ],
565 Vec::new(),
566 Vec::new(),
567 )
568 .unwrap();
569
570 let step_ids = definition
571 .steps_for_state(&state_id("drafting"))
572 .map(CeremonyStep::id)
573 .cloned()
574 .collect::<Vec<_>>();
575
576 assert_eq!(
577 step_ids,
578 vec![
579 step_id("write_plan"),
580 step_id("challenge_plan"),
581 step_id("archive_plan"),
582 ]
583 );
584 }
585
586 #[test]
587 fn rejects_definitions_without_exactly_one_initial_state() {
588 let err = definition(
589 vec![
590 CeremonyState::new(state_id("one"), CeremonyStateKind::Initial),
591 CeremonyState::new(state_id("two"), CeremonyStateKind::Initial),
592 CeremonyState::terminal(state_id("done")),
593 ],
594 Vec::new(),
595 Vec::new(),
596 Vec::new(),
597 Vec::new(),
598 )
599 .unwrap_err();
600
601 assert!(matches!(err, DomainError::InvariantViolated { .. }));
602 }
603
604 #[test]
605 fn rejects_terminal_states_with_outgoing_transitions() {
606 let transition = CeremonyTransition::new(
607 state_id("done"),
608 state_id("drafting"),
609 trigger("restart"),
610 Vec::new(),
611 )
612 .unwrap();
613
614 let err = definition(
615 vec![
616 CeremonyState::initial(state_id("drafting")),
617 CeremonyState::terminal(state_id("done")),
618 ],
619 vec![transition],
620 Vec::new(),
621 Vec::new(),
622 Vec::new(),
623 )
624 .unwrap_err();
625
626 assert!(matches!(err, DomainError::InvariantViolated { .. }));
627 }
628
629 #[test]
630 fn rejects_roles_that_reference_unknown_steps() {
631 let role = role(vec![RoleAction::step(step_id("missing"))]);
632
633 let err = definition(
634 vec![
635 CeremonyState::initial(state_id("drafting")),
636 CeremonyState::terminal(state_id("done")),
637 ],
638 Vec::new(),
639 Vec::new(),
640 Vec::new(),
641 vec![role],
642 )
643 .unwrap_err();
644
645 assert!(matches!(
646 err,
647 DomainError::NotFound {
648 what: "ceremony_role.step_action"
649 }
650 ));
651 }
652
653 #[test]
654 fn rejects_empty_states_collection() {
655 let err =
656 definition(Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()).unwrap_err();
657
658 assert!(matches!(
659 err,
660 DomainError::EmptyCollection {
661 field: "ceremony_definition.states"
662 }
663 ));
664 }
665
666 #[test]
667 fn rejects_transition_referencing_unknown_from_state() {
668 let transition = CeremonyTransition::new(
669 state_id("ghost"),
670 state_id("done"),
671 trigger("finish"),
672 Vec::new(),
673 )
674 .unwrap();
675
676 let err = definition(
677 vec![
678 CeremonyState::initial(state_id("drafting")),
679 CeremonyState::terminal(state_id("done")),
680 ],
681 vec![transition],
682 Vec::new(),
683 Vec::new(),
684 Vec::new(),
685 )
686 .unwrap_err();
687
688 assert!(matches!(
689 err,
690 DomainError::NotFound {
691 what: "ceremony_transition.from_state"
692 }
693 ));
694 }
695
696 #[test]
697 fn rejects_transition_referencing_unknown_to_state() {
698 let transition = CeremonyTransition::new(
699 state_id("drafting"),
700 state_id("ghost"),
701 trigger("finish"),
702 Vec::new(),
703 )
704 .unwrap();
705
706 let err = definition(
707 vec![
708 CeremonyState::initial(state_id("drafting")),
709 CeremonyState::terminal(state_id("done")),
710 ],
711 vec![transition],
712 Vec::new(),
713 Vec::new(),
714 Vec::new(),
715 )
716 .unwrap_err();
717
718 assert!(matches!(
719 err,
720 DomainError::NotFound {
721 what: "ceremony_transition.to_state"
722 }
723 ));
724 }
725
726 #[test]
727 fn rejects_duplicate_state_trigger_pairs() {
728 let first = CeremonyTransition::new(
729 state_id("drafting"),
730 state_id("done"),
731 trigger("finish"),
732 Vec::new(),
733 )
734 .unwrap();
735 let duplicate = CeremonyTransition::new(
736 state_id("drafting"),
737 state_id("done"),
738 trigger("finish"),
739 Vec::new(),
740 )
741 .unwrap();
742
743 let err = definition(
744 vec![
745 CeremonyState::initial(state_id("drafting")),
746 CeremonyState::terminal(state_id("done")),
747 ],
748 vec![first, duplicate],
749 Vec::new(),
750 Vec::new(),
751 Vec::new(),
752 )
753 .unwrap_err();
754
755 assert!(matches!(
756 err,
757 DomainError::AlreadyExists {
758 what: "ceremony_transition.state_trigger"
759 }
760 ));
761 }
762
763 #[test]
764 fn rejects_transition_referencing_unknown_guard() {
765 let transition = CeremonyTransition::new(
766 state_id("drafting"),
767 state_id("done"),
768 trigger("finish"),
769 vec![guard_name("absent")],
770 )
771 .unwrap();
772
773 let err = definition(
774 vec![
775 CeremonyState::initial(state_id("drafting")),
776 CeremonyState::terminal(state_id("done")),
777 ],
778 vec![transition],
779 Vec::new(),
780 Vec::new(),
781 Vec::new(),
782 )
783 .unwrap_err();
784
785 assert!(matches!(
786 err,
787 DomainError::NotFound {
788 what: "ceremony_transition.guard"
789 }
790 ));
791 }
792
793 #[test]
794 fn rejects_step_referencing_unknown_state() {
795 let err = definition(
796 vec![
797 CeremonyState::initial(state_id("drafting")),
798 CeremonyState::terminal(state_id("done")),
799 ],
800 Vec::new(),
801 vec![step("plan", "ghost")],
802 Vec::new(),
803 Vec::new(),
804 )
805 .unwrap_err();
806
807 assert!(matches!(
808 err,
809 DomainError::NotFound {
810 what: "ceremony_step.state"
811 }
812 ));
813 }
814
815 #[test]
816 fn rejects_role_that_references_unknown_transition_trigger() {
817 let role = role(vec![RoleAction::transition(trigger("ghost"))]);
818
819 let err = definition(
820 vec![
821 CeremonyState::initial(state_id("drafting")),
822 CeremonyState::terminal(state_id("done")),
823 ],
824 Vec::new(),
825 Vec::new(),
826 Vec::new(),
827 vec![role],
828 )
829 .unwrap_err();
830
831 assert!(matches!(
832 err,
833 DomainError::NotFound {
834 what: "ceremony_role.transition_action"
835 }
836 ));
837 }
838
839 #[test]
840 fn resolves_role_authorised_for_a_step() {
841 let definition = valid_definition();
842
843 assert_eq!(
844 definition.role_id_for_step(&step_id("plan")).unwrap(),
845 RoleId::new("facilitator").unwrap()
846 );
847 }
848
849 #[test]
850 fn rejects_step_with_no_authorised_role() {
851 let definition = valid_definition();
852
853 let err = definition
854 .role_id_for_step(&step_id("unowned"))
855 .unwrap_err();
856
857 assert!(matches!(
858 err,
859 DomainError::InvariantViolated {
860 reason: "no ceremony role can execute step"
861 }
862 ));
863 }
864
865 #[test]
866 fn resolves_role_authorised_for_a_transition() {
867 let definition = valid_definition();
868
869 assert_eq!(
870 definition
871 .role_id_for_transition(&trigger("finish"))
872 .unwrap(),
873 RoleId::new("facilitator").unwrap()
874 );
875 }
876
877 #[test]
878 fn rejects_transition_with_no_authorised_role() {
879 let definition = valid_definition();
880
881 let err = definition
882 .role_id_for_transition(&trigger("unowned"))
883 .unwrap_err();
884
885 assert!(matches!(
886 err,
887 DomainError::InvariantViolated {
888 reason: "no ceremony role can apply transition"
889 }
890 ));
891 }
892
893 #[test]
894 fn selects_guardless_transition_as_immediately_enabled() {
895 let transition = CeremonyTransition::new(
896 state_id("open"),
897 state_id("closed"),
898 trigger("go"),
899 Vec::new(),
900 )
901 .unwrap();
902 let definition = definition(
903 vec![
904 CeremonyState::initial(state_id("open")),
905 CeremonyState::terminal(state_id("closed")),
906 ],
907 vec![transition],
908 Vec::new(),
909 Vec::new(),
910 Vec::new(),
911 )
912 .unwrap();
913
914 let selected = definition
915 .next_satisfied_transition(
916 &state_id("open"),
917 &BTreeMap::new(),
918 &CeremonyContext::empty(),
919 )
920 .expect("guardless transition is always enabled");
921
922 assert_eq!(selected.trigger(), &trigger("go"));
923 }
924
925 #[test]
926 fn skips_transition_whose_guards_are_unsatisfied() {
927 let definition = valid_definition();
928
929 assert!(definition
930 .next_satisfied_transition(
931 &state_id("drafting"),
932 &BTreeMap::new(),
933 &CeremonyContext::empty(),
934 )
935 .is_none());
936 }
937
938 #[test]
939 fn yields_no_transition_out_of_a_terminal_state() {
940 let definition = valid_definition();
941
942 assert!(definition
943 .next_satisfied_transition(
944 &state_id("done"),
945 &BTreeMap::new(),
946 &CeremonyContext::empty(),
947 )
948 .is_none());
949 }
950
951 #[test]
952 fn rejects_guards_that_reference_unknown_steps() {
953 let guard = CeremonyGuard::new(
954 guard_name("unknown_step_done"),
955 GuardCondition::StepStatus {
956 step_id: step_id("missing"),
957 status: StepStatus::Completed,
958 },
959 );
960
961 let err = definition(
962 vec![
963 CeremonyState::initial(state_id("drafting")),
964 CeremonyState::terminal(state_id("done")),
965 ],
966 Vec::new(),
967 Vec::new(),
968 vec![guard],
969 Vec::new(),
970 )
971 .unwrap_err();
972
973 assert!(matches!(
974 err,
975 DomainError::NotFound {
976 what: "ceremony_guard.step"
977 }
978 ));
979 }
980
981 #[test]
987 fn serde_json_emits_sorted_keys() {
988 let mut out_of_order = serde_json::Map::new();
989 out_of_order.insert("zulu".to_owned(), serde_json::Value::from(1));
990 out_of_order.insert("alpha".to_owned(), serde_json::Value::from(2));
991
992 assert_eq!(
993 serde_json::to_string(&serde_json::Value::Object(out_of_order)).unwrap(),
994 r#"{"alpha":2,"zulu":1}"#,
995 "serde_json is no longer emitting sorted keys, so the definition digest is not canonical"
996 );
997 }
998
999 #[test]
1000 fn the_same_definition_always_digests_the_same() {
1001 assert_eq!(
1002 valid_definition().digest().unwrap(),
1003 valid_definition().digest().unwrap()
1004 );
1005 }
1006
1007 #[test]
1008 fn rename_migration_keeps_content_but_changes_the_digest_scheme() {
1009 let definition = valid_definition();
1010 let migration = definition.choreographer_v1_digest_migration().unwrap();
1011
1012 assert_eq!(migration.definition_name(), definition.name());
1013 assert_eq!(migration.definition_version(), definition.version());
1014 assert_eq!(migration.destination(), definition.digest().unwrap());
1015 assert_ne!(migration.source(), migration.destination());
1016 }
1017
1018 #[test]
1019 fn a_material_difference_changes_the_digest() {
1020 let baseline = valid_definition().digest().unwrap();
1021 let renamed = definition(
1022 vec![
1023 CeremonyState::initial(state_id("drafting")),
1024 CeremonyState::terminal(state_id("finished")),
1025 ],
1026 vec![CeremonyTransition::new(
1027 state_id("drafting"),
1028 state_id("finished"),
1029 trigger("finish"),
1030 Vec::new(),
1031 )
1032 .unwrap()],
1033 Vec::new(),
1034 Vec::new(),
1035 Vec::new(),
1036 )
1037 .unwrap()
1038 .digest()
1039 .unwrap();
1040
1041 assert_ne!(baseline, renamed);
1042 }
1043
1044 #[test]
1045 fn transition_order_is_material_to_the_digest() {
1046 let states = || {
1050 vec![
1051 CeremonyState::initial(state_id("drafting")),
1052 CeremonyState::terminal(state_id("done")),
1053 CeremonyState::terminal(state_id("cancelled")),
1054 ]
1055 };
1056 let finish = || {
1057 CeremonyTransition::new(
1058 state_id("drafting"),
1059 state_id("done"),
1060 trigger("finish"),
1061 Vec::new(),
1062 )
1063 .unwrap()
1064 };
1065 let cancel = || {
1066 CeremonyTransition::new(
1067 state_id("drafting"),
1068 state_id("cancelled"),
1069 trigger("cancel"),
1070 Vec::new(),
1071 )
1072 .unwrap()
1073 };
1074
1075 let first = definition(
1076 states(),
1077 vec![finish(), cancel()],
1078 Vec::new(),
1079 Vec::new(),
1080 Vec::new(),
1081 )
1082 .unwrap();
1083 let swapped = definition(
1084 states(),
1085 vec![cancel(), finish()],
1086 Vec::new(),
1087 Vec::new(),
1088 Vec::new(),
1089 )
1090 .unwrap();
1091
1092 assert_ne!(first.digest().unwrap(), swapped.digest().unwrap());
1093 }
1094
1095 #[test]
1096 fn a_valid_definition_reports_no_findings_at_all() {
1097 let report = valid_definition().analyze();
1098
1099 assert!(report.is_valid());
1100 assert!(report.findings().is_empty());
1101 }
1102
1103 #[test]
1104 fn an_unreachable_state_is_warned_about_without_blocking_construction() {
1105 let definition = definition(
1106 vec![
1107 CeremonyState::initial(state_id("drafting")),
1108 CeremonyState::intermediate(state_id("orphan")),
1109 CeremonyState::terminal(state_id("done")),
1110 ],
1111 vec![
1112 CeremonyTransition::new(
1113 state_id("drafting"),
1114 state_id("done"),
1115 trigger("finish"),
1116 Vec::new(),
1117 )
1118 .unwrap(),
1119 CeremonyTransition::new(
1120 state_id("orphan"),
1121 state_id("done"),
1122 trigger("rescue"),
1123 Vec::new(),
1124 )
1125 .unwrap(),
1126 ],
1127 Vec::new(),
1128 Vec::new(),
1129 Vec::new(),
1130 )
1131 .unwrap();
1132
1133 let report = definition.analyze();
1134 let warnings = report.warnings().collect::<Vec<_>>();
1135
1136 assert!(report.is_valid());
1137 assert_eq!(warnings.len(), 1);
1138 assert_eq!(
1139 warnings[0].locus(),
1140 &CeremonyValidationLocus::state(state_id("orphan"))
1141 );
1142 }
1143
1144 #[test]
1145 fn a_state_that_cannot_reach_a_terminal_is_warned_about() {
1146 let definition = definition(
1147 vec![
1148 CeremonyState::initial(state_id("drafting")),
1149 CeremonyState::intermediate(state_id("stuck")),
1150 CeremonyState::terminal(state_id("done")),
1151 ],
1152 vec![
1153 CeremonyTransition::new(
1154 state_id("drafting"),
1155 state_id("done"),
1156 trigger("finish"),
1157 Vec::new(),
1158 )
1159 .unwrap(),
1160 CeremonyTransition::new(
1161 state_id("drafting"),
1162 state_id("stuck"),
1163 trigger("stall"),
1164 Vec::new(),
1165 )
1166 .unwrap(),
1167 CeremonyTransition::new(
1168 state_id("stuck"),
1169 state_id("stuck"),
1170 trigger("spin"),
1171 Vec::new(),
1172 )
1173 .unwrap(),
1174 ],
1175 Vec::new(),
1176 Vec::new(),
1177 Vec::new(),
1178 )
1179 .unwrap();
1180
1181 let report = definition.analyze();
1182 let warnings = report.warnings().collect::<Vec<_>>();
1183
1184 assert_eq!(warnings.len(), 1);
1185 assert_eq!(
1186 warnings[0].locus(),
1187 &CeremonyValidationLocus::state(state_id("stuck"))
1188 );
1189 }
1190
1191 #[test]
1192 fn a_definition_without_any_terminal_state_is_warned_about() {
1193 let definition = definition(
1194 vec![CeremonyState::initial(state_id("drafting"))],
1195 Vec::new(),
1196 Vec::new(),
1197 Vec::new(),
1198 Vec::new(),
1199 )
1200 .unwrap();
1201
1202 let report = definition.analyze();
1203 let warnings = report.warnings().collect::<Vec<_>>();
1204
1205 assert_eq!(warnings.len(), 1);
1206 assert_eq!(warnings[0].locus(), &CeremonyValidationLocus::Definition);
1207 }
1208
1209 #[test]
1210 fn structural_errors_suppress_reachability_noise() {
1211 let report = definition(
1212 vec![
1213 CeremonyState::initial(state_id("drafting")),
1214 CeremonyState::initial(state_id("also_drafting")),
1215 CeremonyState::terminal(state_id("done")),
1216 ],
1217 Vec::new(),
1218 Vec::new(),
1219 Vec::new(),
1220 Vec::new(),
1221 )
1222 .unwrap_err();
1223
1224 assert!(matches!(
1225 report,
1226 DomainError::InvariantViolated {
1227 reason: "ceremony definition must have exactly one initial state"
1228 }
1229 ));
1230 }
1231}