Skip to main content

made_core/entities/
ceremony_instance.rs

1//! [`CeremonyInstance`] aggregate.
2//!
3//! Runtime state for a single ceremony execution. The aggregate owns
4//! step leases, retry attempts, idempotency keys and state transitions,
5//! so failover remains a domain rule instead of adapter glue.
6//!
7//! A ceremony is its event stream. Every change goes through two
8//! halves: [`CeremonyInstance::decide`] turns a
9//! [`CeremonyCommand`](super::CeremonyCommand) into the
10//! [`CeremonyEvent`]s it would produce, holding every rule and writing
11//! nothing; [`CeremonyInstance::apply`] writes one event and checks
12//! nothing. [`CeremonyInstance::rehydrate`] folds a whole stream. The
13//! mutators callers already use are thin wrappers over that pair. Opening
14//! constructors also return domain refusals when required context is absent.
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use serde::{Deserialize, Serialize};
19use time::OffsetDateTime;
20
21use super::{
22    ceremony_definition::CeremonyDefinition, CeremonyEvent, CeremonyEvidencePack,
23    CeremonyIntervention, PublishedCeremonyDefinition,
24};
25use crate::error::DomainError;
26use crate::ports::CeremonyEvidenceRequest;
27use crate::value_objects::{
28    AuditActorKind, CeremonyContext, CeremonyDefinitionDigest, CeremonyEvidenceSourceId,
29    CeremonyGuardApproval, CeremonyGuardDeferral, CeremonyGuardDeferralContent, CeremonyId,
30    CeremonyInterventionContent, CeremonyInterventionId, CeremonyInterventionKind,
31    CeremonyInterventionProvenance, CeremonyInterventionTarget, CeremonyName,
32    CeremonyParticipantBinding, CeremonyReason, CeremonyReasonKind, CeremonyRecordRef,
33    CeremonyTransitionRecord, CeremonyVersion, GuardName, IdempotencyKey, MemoryConfidence,
34    RoleAction, RoleId, SessionRecollection, Specialty, StateId, StateIteration, StateVisit,
35    StepAttempt, StepExecutionRecord, StepId, StepLease, StepResult, TransitionTrigger,
36};
37
38mod decisions;
39mod fold;
40mod guard_decisions;
41mod interventions;
42mod invariants;
43mod participant_bindings;
44mod role_resolution;
45#[cfg(test)]
46mod role_resolution_tests;
47mod step_claims;
48mod step_execution;
49mod transitions;
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct CeremonyInstance {
53    id: CeremonyId,
54    definition_name: CeremonyName,
55    definition_version: CeremonyVersion,
56    current_state: StateId,
57    #[serde(default, skip_serializing_if = "StateIteration::is_first")]
58    current_state_iteration: StateIteration,
59    #[serde(default, skip_serializing_if = "StateVisit::is_first")]
60    current_state_visit: StateVisit,
61    step_records: BTreeMap<StepId, StepExecutionRecord>,
62    /// Prior executed records, including repeated work and superseded visits.
63    ///
64    /// Retries within one visit remain represented by their attempt number and
65    /// the audit journal. A new visit archives every executed destination record,
66    /// including failed or expired work; repetition archives successful work.
67    /// Neither operation overwrites the result that preceded the current record.
68    #[serde(default)]
69    step_record_history: BTreeMap<StepId, Vec<StepExecutionRecord>>,
70    #[serde(default)]
71    interventions: Vec<CeremonyIntervention>,
72    #[serde(default)]
73    guard_deferrals: Vec<CeremonyGuardDeferral>,
74    /// Who let each human guard through.
75    ///
76    /// Kept beside the context rather than inside it. The context is
77    /// what a guard is evaluated against and already holds "this one
78    /// is approved"; sessions written before this existed carry that
79    /// and nothing else, and moving approval out of the context would
80    /// have made every one of them unapproved on the next read. So the
81    /// context stays the state, and this is the event that produced
82    /// it.
83    #[serde(default)]
84    guard_approvals: Vec<CeremonyGuardApproval>,
85    /// Every move this session made, in order.
86    ///
87    /// The current state says where a session is; this says how it got
88    /// there. Without it nothing could point at a move, so nothing
89    /// could say why one happened — and "why did this resolve" is the
90    /// question the whole thing is for.
91    #[serde(default)]
92    transitions: Vec<CeremonyTransitionRecord>,
93    /// Why one thing here led to another.
94    ///
95    /// Kept apart from the records rather than inside them, because a
96    /// reason is an edge and not a field: it belongs to the pair, and
97    /// putting it on either end would make it readable but not
98    /// followable.
99    #[serde(default)]
100    reasons: Vec<CeremonyReason>,
101    /// Who sits in each seat for this session, where anyone was
102    /// seated. A role with no binding is played the way the definition
103    /// says, which is the usual case and not a lesser one.
104    #[serde(default)]
105    participant_bindings: BTreeMap<RoleId, CeremonyParticipantBinding>,
106    context: CeremonyContext,
107    /// What earlier sessions in this session's memory scope decided,
108    /// as this session was told at its opening.
109    ///
110    /// Absent for a session that recalled nothing, which is every
111    /// session that declares no scope of its own: the default scope is
112    /// the session's own id, and nobody else writes there. Kept as it
113    /// was read rather than re-read on demand — what a session was told
114    /// is part of what it did, and answering later with today's memory
115    /// would judge the session by knowledge it did not have.
116    #[serde(default)]
117    recollection: Option<SessionRecollection>,
118    idempotency_keys: BTreeSet<IdempotencyKey>,
119    #[serde(with = "time::serde::rfc3339")]
120    created_at: OffsetDateTime,
121    #[serde(with = "time::serde::rfc3339")]
122    updated_at: OffsetDateTime,
123    #[serde(with = "time::serde::rfc3339::option")]
124    completed_at: Option<OffsetDateTime>,
125    /// The published definition this instance is bound to, when it was
126    /// started from one.
127    ///
128    /// Absent for an instance started from a definition handed in at
129    /// the time — which is a real and useful way to work, and not the
130    /// same thing. Recording which of the two happened is the point: a
131    /// name and a version identify a published definition only while
132    /// publication is immutable, and an instance that also carries the
133    /// digest can be checked against the definition rather than trusted
134    /// to have run it.
135    #[serde(default)]
136    bound_definition: Option<CeremonyDefinitionDigest>,
137}
138
139impl CeremonyInstance {
140    /// Start from a definition supplied for this run.
141    ///
142    /// Nothing binds the instance to a definition that can be looked up
143    /// later; that is what [`Self::start_bound`] is for. The instance
144    /// is the fold of the opening event [`Self::decide_start`] yields.
145    pub fn start(
146        id: CeremonyId,
147        definition: &CeremonyDefinition,
148        context: CeremonyContext,
149        now: OffsetDateTime,
150    ) -> Result<Self, DomainError> {
151        Ok(Self::from_started(&Self::opening(
152            id, definition, context, now, None,
153        )?))
154    }
155
156    /// Start from a published definition, recording its digest.
157    ///
158    /// The digest travels with the instance so a later reader can
159    /// verify which definition ran instead of taking the name and
160    /// version on trust.
161    pub fn start_bound(
162        id: CeremonyId,
163        published: &PublishedCeremonyDefinition,
164        context: CeremonyContext,
165        now: OffsetDateTime,
166    ) -> Result<Self, DomainError> {
167        Ok(Self::from_started(&Self::opening(
168            id,
169            published.definition(),
170            context,
171            now,
172            Some(published.digest()),
173        )?))
174    }
175
176    /// The digest of the published definition this instance runs, if it
177    /// was started from one.
178    #[must_use]
179    pub fn bound_definition(&self) -> Option<CeremonyDefinitionDigest> {
180        self.bound_definition
181    }
182
183    /// What earlier sessions in this session's scope decided, as this
184    /// session was told when it opened.
185    #[must_use]
186    pub fn recollection(&self) -> Option<&SessionRecollection> {
187        self.recollection.as_ref()
188    }
189
190    /// Whether this instance runs a definition that can be looked up
191    /// and checked, rather than one supplied for the run.
192    #[must_use]
193    pub fn is_bound_to_a_published_definition(&self) -> bool {
194        self.bound_definition.is_some()
195    }
196
197    #[must_use]
198    pub fn id(&self) -> &CeremonyId {
199        &self.id
200    }
201
202    #[must_use]
203    pub fn definition_name(&self) -> &CeremonyName {
204        &self.definition_name
205    }
206
207    #[must_use]
208    pub fn definition_version(&self) -> &CeremonyVersion {
209        &self.definition_version
210    }
211
212    #[must_use]
213    pub fn current_state(&self) -> &StateId {
214        &self.current_state
215    }
216
217    #[must_use]
218    pub fn current_state_visit(&self) -> StateVisit {
219        self.current_state_visit
220    }
221
222    #[must_use]
223    pub fn current_state_iteration(&self) -> StateIteration {
224        self.current_state_iteration
225    }
226
227    #[must_use]
228    pub fn state_work_is_complete(&self, definition: &CeremonyDefinition) -> bool {
229        definition.steps_for_state(&self.current_state).all(|step| {
230            self.step_records
231                .get(step.id())
232                .is_some_and(|record| record.status().is_success())
233        }) && definition.repeat_requirements_are_satisfied(&self.current_state, &self.step_records)
234    }
235
236    #[must_use]
237    pub fn state_repeat_condition_is_satisfied(&self, definition: &CeremonyDefinition) -> bool {
238        let Some(policy) = definition
239            .state(&self.current_state)
240            .and_then(|state| state.repeat_policy())
241        else {
242            return true;
243        };
244        self.step_records
245            .get(policy.until().step_id())
246            .is_some_and(|record| policy.is_satisfied(record))
247    }
248
249    #[must_use]
250    pub fn state_repeat_permits_transition(&self, definition: &CeremonyDefinition) -> bool {
251        definition
252            .state(&self.current_state)
253            .and_then(|state| state.repeat_policy())
254            .is_none()
255            || (self.state_work_is_complete(definition)
256                && self.state_repeat_condition_is_satisfied(definition))
257    }
258
259    #[must_use]
260    pub fn state_repeat_limit_reached(&self, definition: &CeremonyDefinition) -> bool {
261        let Some(policy) = definition
262            .state(&self.current_state)
263            .and_then(|state| state.repeat_policy())
264        else {
265            return false;
266        };
267        self.state_work_is_complete(definition)
268            && !self.state_repeat_condition_is_satisfied(definition)
269            && !policy.permits_another_iteration(self.current_state_iteration)
270    }
271
272    #[must_use]
273    pub fn step_records(&self) -> &BTreeMap<StepId, StepExecutionRecord> {
274        &self.step_records
275    }
276
277    #[must_use]
278    pub fn step_record(&self, step_id: &StepId) -> Option<&StepExecutionRecord> {
279        self.step_records.get(step_id)
280    }
281
282    /// Finished iterations before the current record, in execution order.
283    #[must_use]
284    pub fn step_record_history(&self, step_id: &StepId) -> &[StepExecutionRecord] {
285        self.step_record_history
286            .get(step_id)
287            .map(Vec::as_slice)
288            .unwrap_or_default()
289    }
290
291    /// Whether a repeating step consumed its last permitted iteration without
292    /// satisfying its declared stop condition.
293    #[must_use]
294    pub fn step_repeat_limit_reached(
295        &self,
296        definition: &CeremonyDefinition,
297        step_id: &StepId,
298    ) -> bool {
299        let Some(step) = definition.step(step_id) else {
300            return false;
301        };
302        let Some(policy) = step.repeat_policy() else {
303            return false;
304        };
305        let Some(record) = self.step_record(step_id) else {
306            return false;
307        };
308        record.status().is_success()
309            && !policy.is_satisfied(record.output())
310            && !policy.permits_another_iteration(record.iteration())
311    }
312
313    #[must_use]
314    pub fn interventions(&self) -> &[CeremonyIntervention] {
315        &self.interventions
316    }
317
318    #[must_use]
319    pub fn guard_deferrals(&self) -> &[CeremonyGuardDeferral] {
320        &self.guard_deferrals
321    }
322
323    /// Who let each human guard through, in the order they did.
324    ///
325    /// Empty for a session written before approvals recorded an
326    /// approver, which is the truth about those sessions rather than a
327    /// gap to paper over.
328    #[must_use]
329    pub fn guard_approvals(&self) -> &[CeremonyGuardApproval] {
330        &self.guard_approvals
331    }
332
333    /// Every move this session made, in the order it made them.
334    #[must_use]
335    pub fn transitions(&self) -> &[CeremonyTransitionRecord] {
336        &self.transitions
337    }
338
339    /// Why one thing here led to another.
340    #[must_use]
341    pub fn reasons(&self) -> &[CeremonyReason] {
342        &self.reasons
343    }
344
345    #[must_use]
346    pub fn intervention(
347        &self,
348        intervention_id: &CeremonyInterventionId,
349    ) -> Option<&CeremonyIntervention> {
350        self.interventions
351            .iter()
352            .find(|intervention| intervention.id() == intervention_id)
353    }
354
355    #[must_use]
356    pub fn context(&self) -> &CeremonyContext {
357        &self.context
358    }
359
360    #[must_use]
361    pub fn idempotency_keys(&self) -> &BTreeSet<IdempotencyKey> {
362        &self.idempotency_keys
363    }
364
365    #[must_use]
366    pub fn created_at(&self) -> OffsetDateTime {
367        self.created_at
368    }
369
370    #[must_use]
371    pub fn updated_at(&self) -> OffsetDateTime {
372        self.updated_at
373    }
374
375    #[must_use]
376    pub fn completed_at(&self) -> Option<OffsetDateTime> {
377        self.completed_at
378    }
379
380    #[must_use]
381    pub fn is_terminal(&self, definition: &CeremonyDefinition) -> bool {
382        self.matches_definition(definition) && definition.is_terminal_state(&self.current_state)
383    }
384
385    #[must_use]
386    pub fn is_completed(&self, definition: &CeremonyDefinition) -> bool {
387        self.is_terminal(definition) && self.completed_at.is_some()
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::value_objects::{
395        Attributes, CeremonyGuard, CeremonyState, CeremonyStep, CeremonyTransition, GuardCondition,
396        GuardName, LeaseOwnerId, RepeatUntilCondition, RetryPolicy, StepHandlerConfig,
397        StepHandlerKind, StepIteration, StepOutput, StepOutputField, StepRepeatPolicy, StepStatus,
398    };
399    use serde_json::json;
400    use time::macros::datetime;
401
402    fn now() -> OffsetDateTime {
403        datetime!(2026-06-06 12:00:00 UTC)
404    }
405
406    fn state_id(raw: &str) -> StateId {
407        StateId::new(raw).unwrap()
408    }
409
410    fn step_id(raw: &str) -> StepId {
411        StepId::new(raw).unwrap()
412    }
413
414    fn trigger(raw: &str) -> TransitionTrigger {
415        TransitionTrigger::new(raw).unwrap()
416    }
417
418    fn role_id(raw: &str) -> RoleId {
419        RoleId::new(raw).unwrap()
420    }
421
422    fn guard_name(raw: &str) -> GuardName {
423        GuardName::new(raw).unwrap()
424    }
425
426    fn handler_kind() -> StepHandlerKind {
427        StepHandlerKind::new("multiagent_round").unwrap()
428    }
429
430    fn retrying_step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
431        CeremonyStep::new(
432            step_id(raw_step_id),
433            state_id(raw_state_id),
434            handler_kind(),
435            StepHandlerConfig::empty(),
436            RetryPolicy::new(
437                StepAttempt::new(3).unwrap(),
438                crate::value_objects::DurationMs::ZERO,
439            ),
440            None,
441        )
442    }
443
444    fn single_attempt_step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
445        CeremonyStep::new(
446            step_id(raw_step_id),
447            state_id(raw_state_id),
448            handler_kind(),
449            StepHandlerConfig::empty(),
450            RetryPolicy::single_attempt(),
451            None,
452        )
453    }
454
455    fn repeating_plan(max_iterations: u32) -> CeremonyStep {
456        retrying_step("plan", "drafting").with_repeat_policy(StepRepeatPolicy::new(
457            RepeatUntilCondition::output_field_equals(
458                StepOutputField::new("ready").unwrap(),
459                json!(true),
460            ),
461            StepIteration::new(max_iterations).unwrap(),
462        ))
463    }
464
465    fn readiness_output(ready: bool) -> StepOutput {
466        StepOutput::new(
467            Attributes::new(std::collections::BTreeMap::from([(
468                "ready".to_owned(),
469                json!(ready),
470            )]))
471            .unwrap(),
472        )
473    }
474
475    fn lease(
476        raw_owner_id: &str,
477        raw_key: &str,
478        acquired_at: OffsetDateTime,
479        expires_at: OffsetDateTime,
480    ) -> StepLease {
481        StepLease::new(
482            LeaseOwnerId::new(raw_owner_id).unwrap(),
483            IdempotencyKey::new(raw_key).unwrap(),
484            acquired_at,
485            expires_at,
486        )
487        .unwrap()
488    }
489
490    fn role(actions: Vec<RoleAction>) -> crate::value_objects::CeremonyRole {
491        crate::value_objects::CeremonyRole::new(role_id("facilitator"), actions).unwrap()
492    }
493
494    fn definition_with_steps(steps: Vec<CeremonyStep>) -> CeremonyDefinition {
495        let plan_done = CeremonyGuard::new(
496            guard_name("plan_done"),
497            GuardCondition::StepStatus {
498                step_id: step_id("plan"),
499                status: StepStatus::Completed,
500            },
501        );
502        let finish = CeremonyTransition::new(
503            state_id("drafting"),
504            state_id("done"),
505            trigger("finish"),
506            vec![plan_done.name().clone()],
507        )
508        .unwrap();
509        let role = role(vec![
510            RoleAction::step(step_id("plan")),
511            RoleAction::transition(finish.trigger().clone()),
512            RoleAction::request_intervention(),
513        ]);
514        let observer = crate::value_objects::CeremonyRole::new(
515            role_id("observer"),
516            vec![RoleAction::respond_to_intervention()],
517        )
518        .unwrap();
519
520        CeremonyDefinition::new(
521            crate::value_objects::CeremonyName::new("planning_ceremony").unwrap(),
522            CeremonyVersion::v1(),
523            None,
524            Vec::new(),
525            Vec::new(),
526            vec![
527                CeremonyState::initial(state_id("drafting")),
528                CeremonyState::intermediate(state_id("review")),
529                CeremonyState::terminal(state_id("done")),
530            ],
531            vec![finish],
532            steps,
533            vec![plan_done],
534            vec![role, observer],
535        )
536        .unwrap()
537    }
538
539    fn definition() -> CeremonyDefinition {
540        definition_with_steps(vec![
541            retrying_step("plan", "drafting"),
542            single_attempt_step("review_step", "review"),
543        ])
544    }
545
546    /// The smallest ceremony that waits on a person: one guard, one
547    /// transition it blocks, one seat allowed to fire it.
548    fn definition_with_human_guard(approval: &CeremonyGuard) -> CeremonyDefinition {
549        let finish = CeremonyTransition::new(
550            state_id("drafting"),
551            state_id("done"),
552            trigger("approve"),
553            vec![approval.name().clone()],
554        )
555        .unwrap();
556        CeremonyDefinition::new(
557            crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
558            CeremonyVersion::v1(),
559            None,
560            Vec::new(),
561            Vec::new(),
562            vec![
563                CeremonyState::initial(state_id("drafting")),
564                CeremonyState::terminal(state_id("done")),
565            ],
566            vec![finish.clone()],
567            Vec::new(),
568            vec![approval.clone()],
569            vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
570        )
571        .unwrap()
572    }
573
574    fn instance(definition: &CeremonyDefinition) -> CeremonyInstance {
575        CeremonyInstance::start(
576            CeremonyId::new("ceremony-1").unwrap(),
577            definition,
578            CeremonyContext::empty(),
579            now(),
580        )
581        .expect("required ceremony inputs")
582    }
583
584    #[test]
585    fn starts_in_initial_state_with_pending_records() {
586        let definition = definition();
587        let instance = instance(&definition);
588
589        assert_eq!(instance.current_state(), &state_id("drafting"));
590        assert_eq!(
591            instance.step_record(&step_id("plan")).unwrap().status(),
592            StepStatus::Pending
593        );
594        assert_eq!(
595            instance
596                .step_record(&step_id("review_step"))
597                .unwrap()
598                .status(),
599            StepStatus::Pending
600        );
601    }
602
603    #[test]
604    fn instances_without_iteration_fields_load_as_the_first_iteration() {
605        let definition = definition();
606        let mut value = serde_json::to_value(instance(&definition)).unwrap();
607        value.as_object_mut().unwrap().remove("step_record_history");
608        for record in value["step_records"].as_object_mut().unwrap().values_mut() {
609            record.as_object_mut().unwrap().remove("iteration");
610        }
611
612        let restored: CeremonyInstance = serde_json::from_value(value).unwrap();
613
614        assert!(restored.step_record_history(&step_id("plan")).is_empty());
615        assert_eq!(
616            restored.step_record(&step_id("plan")).unwrap().iteration(),
617            StepIteration::FIRST
618        );
619    }
620
621    #[test]
622    fn dynamic_intervention_collects_role_scoped_response_and_requester_closes_it() {
623        let definition = definition();
624        let mut instance = instance(&definition);
625        let intervention_id = CeremonyInterventionId::new("queue-check").unwrap();
626        let facilitator = role_id("facilitator");
627        let observer = role_id("observer");
628
629        instance
630            .request_intervention_as(
631                &definition,
632                intervention_id.clone(),
633                facilitator.clone(),
634                CeremonyInterventionKind::Investigation,
635                CeremonyInterventionTarget::roles([observer.clone()]).unwrap(),
636                CeremonyInterventionContent::new(
637                    "Inspect the queue without consuming messages.",
638                    Attributes::empty(),
639                )
640                .unwrap(),
641                now(),
642            )
643            .unwrap();
644        instance
645            .respond_to_intervention_as(
646                &definition,
647                &intervention_id,
648                observer.clone(),
649                CeremonyInterventionContent::new("Queue depth is stable.", Attributes::empty())
650                    .unwrap(),
651                now(),
652            )
653            .unwrap();
654        let selected_intervention_id = CeremonyInterventionId::new("selected-check").unwrap();
655        instance
656            .request_intervention_with_provenance_as(
657                &definition,
658                selected_intervention_id.clone(),
659                facilitator.clone(),
660                CeremonyInterventionKind::Investigation,
661                CeremonyInterventionTarget::roles([observer.clone()]).unwrap(),
662                CeremonyInterventionContent::new(
663                    "Inspect the proposed signal.",
664                    Attributes::empty(),
665                )
666                .unwrap(),
667                Some(CeremonyInterventionProvenance::selected_from(
668                    intervention_id.clone(),
669                    observer.clone(),
670                    observer.clone(),
671                )),
672                now(),
673            )
674            .unwrap();
675        instance
676            .close_intervention_as(&definition, &intervention_id, &facilitator, now())
677            .unwrap();
678
679        let intervention = instance.intervention(&intervention_id).unwrap();
680        assert_eq!(intervention.responses().len(), 1);
681        assert_eq!(
682            intervention.status(),
683            crate::value_objects::CeremonyInterventionStatus::Closed
684        );
685        let provenance = instance
686            .intervention(&selected_intervention_id)
687            .unwrap()
688            .provenance()
689            .unwrap();
690        assert_eq!(provenance.source_intervention_id(), &intervention_id);
691        assert_eq!(provenance.selected_role_id(), &observer);
692    }
693
694    #[test]
695    fn intervention_rejects_roles_without_the_required_capability() {
696        let definition = definition();
697        let mut instance = instance(&definition);
698
699        let error = instance
700            .request_intervention_as(
701                &definition,
702                CeremonyInterventionId::new("not-allowed").unwrap(),
703                role_id("observer"),
704                CeremonyInterventionKind::Opinion,
705                CeremonyInterventionTarget::table(),
706                CeremonyInterventionContent::new("What do you think?", Attributes::empty())
707                    .unwrap(),
708                now(),
709            )
710            .unwrap_err();
711
712        assert!(matches!(error, DomainError::InvariantViolated { .. }));
713    }
714
715    #[test]
716    fn rejects_step_execution_outside_current_state() {
717        let definition = definition();
718        let mut instance = instance(&definition);
719
720        let err = instance
721            .start_step(
722                &definition,
723                &step_id("review_step"),
724                lease(
725                    "runner-1",
726                    "key-1",
727                    now(),
728                    datetime!(2026-06-06 12:05:00 UTC),
729                ),
730                now(),
731            )
732            .unwrap_err();
733
734        assert!(matches!(err, DomainError::InvalidTransition { .. }));
735    }
736
737    #[test]
738    fn completed_step_unlocks_guarded_transition() {
739        let definition = definition();
740        let mut instance = instance(&definition);
741
742        instance
743            .start_step_as(
744                &definition,
745                &role_id("facilitator"),
746                &step_id("plan"),
747                lease(
748                    "runner-1",
749                    "key-1",
750                    now(),
751                    datetime!(2026-06-06 12:05:00 UTC),
752                ),
753                now(),
754            )
755            .unwrap();
756        instance
757            .apply_step_result(
758                &definition,
759                &step_id("plan"),
760                instance.step_claim_fence(&step_id("plan")).unwrap(),
761                StepResult::completed(StepOutput::empty()).unwrap(),
762                datetime!(2026-06-06 12:01:00 UTC),
763            )
764            .unwrap();
765        let state = instance
766            .apply_transition_as(
767                &definition,
768                &role_id("facilitator"),
769                &trigger("finish"),
770                datetime!(2026-06-06 12:02:00 UTC),
771            )
772            .unwrap();
773
774        assert_eq!(state, state_id("done"));
775        assert!(instance.is_completed(&definition));
776    }
777
778    #[test]
779    fn false_repeat_condition_archives_iteration_and_schedules_the_next() {
780        let definition = definition_with_steps(vec![repeating_plan(3)]);
781        let mut instance = instance(&definition);
782
783        instance
784            .start_step(
785                &definition,
786                &step_id("plan"),
787                lease(
788                    "runner-1",
789                    "repeat-1",
790                    now(),
791                    datetime!(2026-06-06 12:05:00 UTC),
792                ),
793                now(),
794            )
795            .unwrap();
796        instance
797            .apply_step_result(
798                &definition,
799                &step_id("plan"),
800                instance.step_claim_fence(&step_id("plan")).unwrap(),
801                StepResult::completed(readiness_output(false)).unwrap(),
802                datetime!(2026-06-06 12:01:00 UTC),
803            )
804            .unwrap();
805
806        let current = instance.step_record(&step_id("plan")).unwrap();
807        assert_eq!(current.status(), StepStatus::Pending);
808        assert_eq!(current.iteration().get(), 2);
809        assert_eq!(current.attempt(), StepAttempt::FIRST);
810        let history = instance.step_record_history(&step_id("plan"));
811        assert_eq!(history.len(), 1);
812        assert_eq!(history[0].iteration(), StepIteration::FIRST);
813        assert_eq!(history[0].output(), &readiness_output(false));
814        assert!(instance
815            .apply_transition(&definition, &trigger("finish"), now())
816            .is_err());
817
818        instance
819            .start_step(
820                &definition,
821                &step_id("plan"),
822                lease(
823                    "runner-1",
824                    "repeat-2",
825                    datetime!(2026-06-06 12:02:00 UTC),
826                    datetime!(2026-06-06 12:07:00 UTC),
827                ),
828                datetime!(2026-06-06 12:02:00 UTC),
829            )
830            .unwrap();
831        instance
832            .apply_step_result(
833                &definition,
834                &step_id("plan"),
835                instance.step_claim_fence(&step_id("plan")).unwrap(),
836                StepResult::completed(readiness_output(true)).unwrap(),
837                datetime!(2026-06-06 12:03:00 UTC),
838            )
839            .unwrap();
840
841        let current = instance.step_record(&step_id("plan")).unwrap();
842        assert_eq!(current.status(), StepStatus::Completed);
843        assert_eq!(current.iteration().get(), 2);
844        assert!(!instance.step_repeat_limit_reached(&definition, &step_id("plan")));
845        assert_eq!(
846            instance
847                .apply_transition(&definition, &trigger("finish"), now())
848                .unwrap(),
849            state_id("done")
850        );
851    }
852
853    #[test]
854    fn repeat_limit_is_terminal_for_the_step_and_blocks_transition() {
855        let definition = definition_with_steps(vec![repeating_plan(2)]);
856        let mut instance = instance(&definition);
857
858        for iteration in 1..=2 {
859            instance
860                .start_step(
861                    &definition,
862                    &step_id("plan"),
863                    lease(
864                        "runner-1",
865                        &format!("limit-{iteration}"),
866                        now(),
867                        datetime!(2026-06-06 12:05:00 UTC),
868                    ),
869                    now(),
870                )
871                .unwrap();
872            instance
873                .apply_step_result(
874                    &definition,
875                    &step_id("plan"),
876                    instance.step_claim_fence(&step_id("plan")).unwrap(),
877                    StepResult::completed(readiness_output(false)).unwrap(),
878                    now(),
879                )
880                .unwrap();
881        }
882
883        assert!(instance.step_repeat_limit_reached(&definition, &step_id("plan")));
884        assert_eq!(
885            instance
886                .step_record(&step_id("plan"))
887                .unwrap()
888                .iteration()
889                .get(),
890            2
891        );
892        assert_eq!(instance.step_record_history(&step_id("plan")).len(), 1);
893        assert!(instance
894            .apply_transition(&definition, &trigger("finish"), now())
895            .is_err());
896        assert!(instance
897            .start_step(
898                &definition,
899                &step_id("plan"),
900                lease(
901                    "runner-1",
902                    "limit-3",
903                    now(),
904                    datetime!(2026-06-06 12:05:00 UTC),
905                ),
906                now(),
907            )
908            .is_err());
909    }
910
911    #[test]
912    fn active_lease_blocks_failover_takeover() {
913        let definition = definition();
914        let mut instance = instance(&definition);
915
916        instance
917            .start_step(
918                &definition,
919                &step_id("plan"),
920                lease(
921                    "runner-1",
922                    "key-1",
923                    now(),
924                    datetime!(2026-06-06 12:05:00 UTC),
925                ),
926                now(),
927            )
928            .unwrap();
929        let err = instance
930            .start_step(
931                &definition,
932                &step_id("plan"),
933                lease(
934                    "runner-2",
935                    "key-2",
936                    datetime!(2026-06-06 12:01:00 UTC),
937                    datetime!(2026-06-06 12:06:00 UTC),
938                ),
939                datetime!(2026-06-06 12:01:00 UTC),
940            )
941            .unwrap_err();
942
943        assert!(matches!(err, DomainError::InvariantViolated { .. }));
944        assert_eq!(
945            instance
946                .step_record(&step_id("plan"))
947                .unwrap()
948                .lease()
949                .unwrap()
950                .owner_id()
951                .as_str(),
952            "runner-1"
953        );
954    }
955
956    #[test]
957    fn expired_lease_allows_failover_takeover_with_next_attempt() {
958        let definition = definition();
959        let mut instance = instance(&definition);
960
961        instance
962            .start_step(
963                &definition,
964                &step_id("plan"),
965                lease(
966                    "runner-1",
967                    "key-1",
968                    now(),
969                    datetime!(2026-06-06 12:05:00 UTC),
970                ),
971                now(),
972            )
973            .unwrap();
974        let attempt = instance
975            .start_step(
976                &definition,
977                &step_id("plan"),
978                lease(
979                    "runner-2",
980                    "key-2",
981                    datetime!(2026-06-06 12:06:00 UTC),
982                    datetime!(2026-06-06 12:11:00 UTC),
983                ),
984                datetime!(2026-06-06 12:06:00 UTC),
985            )
986            .unwrap();
987
988        assert_eq!(attempt, StepAttempt::new(2).unwrap());
989        let record = instance.step_record(&step_id("plan")).unwrap();
990        assert_eq!(record.attempt(), StepAttempt::new(2).unwrap());
991        assert_eq!(record.lease().unwrap().owner_id().as_str(), "runner-2");
992    }
993
994    #[test]
995    fn approving_a_guard_the_ceremony_never_declared_is_refused() {
996        let approval =
997            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
998        let finish = CeremonyTransition::new(
999            state_id("drafting"),
1000            state_id("done"),
1001            trigger("approve"),
1002            vec![approval.name().clone()],
1003        )
1004        .unwrap();
1005        let definition = CeremonyDefinition::new(
1006            crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1007            CeremonyVersion::v1(),
1008            None,
1009            Vec::new(),
1010            Vec::new(),
1011            vec![
1012                CeremonyState::initial(state_id("drafting")),
1013                CeremonyState::terminal(state_id("done")),
1014            ],
1015            vec![finish.clone()],
1016            Vec::new(),
1017            vec![approval],
1018            vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1019        )
1020        .unwrap();
1021        let mut instance = instance(&definition);
1022
1023        // This used to succeed and write `not_a_guard: true` into the
1024        // session context: a caller could put any key at all there,
1025        // and a typo answered "approved" while leaving a session that
1026        // would never move.
1027        assert!(matches!(
1028            instance.approve_guard(
1029                &definition,
1030                &guard_name("not_a_guard"),
1031                role_id("facilitator"),
1032                AuditActorKind::Human,
1033                now()
1034            ),
1035            Err(DomainError::NotFound {
1036                what: "ceremony_guard"
1037            })
1038        ));
1039        assert!(!instance
1040            .context()
1041            .is_guard_approved(&guard_name("not_a_guard")));
1042    }
1043
1044    #[test]
1045    fn human_approval_guard_uses_typed_context() {
1046        let approval =
1047            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1048        let finish = CeremonyTransition::new(
1049            state_id("drafting"),
1050            state_id("done"),
1051            trigger("approve"),
1052            vec![approval.name().clone()],
1053        )
1054        .unwrap();
1055        let definition = CeremonyDefinition::new(
1056            crate::value_objects::CeremonyName::new("approval_ceremony").unwrap(),
1057            CeremonyVersion::v1(),
1058            None,
1059            Vec::new(),
1060            Vec::new(),
1061            vec![
1062                CeremonyState::initial(state_id("drafting")),
1063                CeremonyState::terminal(state_id("done")),
1064            ],
1065            vec![finish.clone()],
1066            Vec::new(),
1067            vec![approval.clone()],
1068            vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1069        )
1070        .unwrap();
1071        let mut instance = instance(&definition);
1072
1073        assert!(matches!(
1074            instance.apply_transition(&definition, &trigger("approve"), now()),
1075            Err(DomainError::InvariantViolated { .. })
1076        ));
1077        instance
1078            .approve_guard(
1079                &definition,
1080                approval.name(),
1081                role_id("facilitator"),
1082                AuditActorKind::Human,
1083                datetime!(2026-06-06 12:01:00 UTC),
1084            )
1085            .unwrap();
1086        instance
1087            .apply_transition(
1088                &definition,
1089                &trigger("approve"),
1090                datetime!(2026-06-06 12:02:00 UTC),
1091            )
1092            .unwrap();
1093
1094        assert!(instance.is_completed(&definition));
1095    }
1096
1097    #[test]
1098    fn human_guard_deferral_preserves_uncertainty_without_approving() {
1099        let approval =
1100            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1101        let finish = CeremonyTransition::new(
1102            state_id("drafting"),
1103            state_id("done"),
1104            trigger("approve"),
1105            vec![approval.name().clone()],
1106        )
1107        .unwrap();
1108        let definition = CeremonyDefinition::new(
1109            crate::value_objects::CeremonyName::new("deferral_ceremony").unwrap(),
1110            CeremonyVersion::v1(),
1111            None,
1112            Vec::new(),
1113            Vec::new(),
1114            vec![
1115                CeremonyState::initial(state_id("drafting")),
1116                CeremonyState::terminal(state_id("done")),
1117            ],
1118            vec![finish.clone()],
1119            Vec::new(),
1120            vec![approval.clone()],
1121            vec![role(vec![RoleAction::transition(finish.trigger().clone())])],
1122        )
1123        .unwrap();
1124        let mut instance = instance(&definition);
1125
1126        instance
1127            .defer_guard(
1128                &definition,
1129                approval.name().clone(),
1130                CeremonyGuardDeferralContent::new(
1131                    "I do not know.",
1132                    "I cannot explain how the issue was resolved.",
1133                    vec!["New evidence explains the resolution.".to_owned()],
1134                )
1135                .unwrap(),
1136                role_id("facilitator"),
1137                AuditActorKind::Human,
1138                datetime!(2026-06-06 12:01:00 UTC),
1139            )
1140            .unwrap();
1141
1142        assert!(!instance.context().is_guard_approved(approval.name()));
1143        assert!(instance
1144            .apply_transition(&definition, &trigger("approve"), now())
1145            .is_err());
1146        let deferral = &instance.guard_deferrals()[0];
1147        assert_eq!(deferral.guard_name(), approval.name());
1148        assert_eq!(deferral.content().statement(), "I do not know.");
1149    }
1150    /// An approval that names nobody is a receipt for a human decision
1151    /// nobody can be shown to have taken. This is that made checkable.
1152    #[test]
1153    fn approving_a_human_guard_records_the_seat_that_did_it() {
1154        let approval =
1155            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1156        let definition = definition_with_human_guard(&approval);
1157        let mut instance = instance(&definition);
1158
1159        instance
1160            .approve_guard(
1161                &definition,
1162                approval.name(),
1163                role_id("facilitator"),
1164                AuditActorKind::Human,
1165                datetime!(2026-06-06 12:01:00 UTC),
1166            )
1167            .unwrap();
1168
1169        let [recorded] = instance.guard_approvals() else {
1170            panic!(
1171                "expected one approval, got {:?}",
1172                instance.guard_approvals()
1173            );
1174        };
1175        assert_eq!(recorded.guard_name(), approval.name());
1176        assert_eq!(recorded.approved_by(), &role_id("facilitator"));
1177        assert_eq!(recorded.approved_at(), datetime!(2026-06-06 12:01:00 UTC));
1178        assert!(instance.context().is_guard_approved(approval.name()));
1179    }
1180
1181    /// A seat this session does not have cannot approve anything on it.
1182    /// Weaker than the capability check the other verbs use, and
1183    /// deliberately so — but not so weak that any string will do.
1184    #[test]
1185    fn a_seat_the_definition_does_not_declare_cannot_approve() {
1186        let approval =
1187            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1188        let definition = definition_with_human_guard(&approval);
1189        let mut instance = instance(&definition);
1190
1191        let outcome = instance.approve_guard(
1192            &definition,
1193            approval.name(),
1194            role_id("someone-who-is-not-here"),
1195            AuditActorKind::Human,
1196            now(),
1197        );
1198
1199        assert!(matches!(
1200            outcome,
1201            Err(DomainError::NotFound {
1202                what: "ceremony_role"
1203            })
1204        ));
1205        assert!(instance.guard_approvals().is_empty());
1206        assert!(!instance.context().is_guard_approved(approval.name()));
1207    }
1208    /// A session with one agenda item and one contribution to it —
1209    /// the smallest thing that has something to explain.
1210    fn session_with_a_contribution(
1211        definition: &CeremonyDefinition,
1212    ) -> (CeremonyInstance, CeremonyInterventionId) {
1213        let mut instance = instance(definition);
1214        let agenda_item = CeremonyInterventionId::new("queue-check").unwrap();
1215        instance
1216            .request_intervention_as(
1217                definition,
1218                agenda_item.clone(),
1219                role_id("facilitator"),
1220                CeremonyInterventionKind::Investigation,
1221                CeremonyInterventionTarget::roles([role_id("observer")]).unwrap(),
1222                CeremonyInterventionContent::new("Inspect the queue.", Attributes::empty())
1223                    .unwrap(),
1224                now(),
1225            )
1226            .unwrap();
1227        instance
1228            .respond_to_intervention_as(
1229                definition,
1230                &agenda_item,
1231                role_id("observer"),
1232                CeremonyInterventionContent::new("Queue depth is stable.", Attributes::empty())
1233                    .unwrap(),
1234                now(),
1235            )
1236            .unwrap();
1237        (instance, agenda_item)
1238    }
1239
1240    /// The one reason the engine sees on its own, and it records it
1241    /// without being asked.
1242    #[test]
1243    fn a_contribution_is_recorded_as_answering_its_agenda_item() {
1244        let definition = definition();
1245        let (instance, agenda_item) = session_with_a_contribution(&definition);
1246
1247        let [answered] = instance.reasons() else {
1248            panic!("expected exactly one reason, got {:?}", instance.reasons());
1249        };
1250        assert_eq!(answered.kind(), CeremonyReasonKind::Answers);
1251        assert_eq!(
1252            answered.from(),
1253            &CeremonyRecordRef::contribution(agenda_item.clone(), 0)
1254        );
1255        assert_eq!(answered.to(), &CeremonyRecordRef::agenda_item(agenda_item));
1256        assert_eq!(
1257            answered.asserted_by(),
1258            None,
1259            "the engine observed it; naming a seat would be inventing one"
1260        );
1261    }
1262
1263    /// Structure is not a judgement. A seat able to assert it could
1264    /// rewrite the shape of the session by relabelling it.
1265    #[test]
1266    fn a_seat_cannot_assert_what_only_the_engine_observes() {
1267        let definition = definition();
1268        let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1269
1270        let outcome = instance.assert_reason_as(
1271            &definition,
1272            role_id("observer"),
1273            CeremonyRecordRef::contribution(agenda_item.clone(), 0),
1274            CeremonyRecordRef::agenda_item(agenda_item),
1275            CeremonyReasonKind::Answers,
1276            "because I say it does",
1277            MemoryConfidence::High,
1278            now(),
1279        );
1280
1281        assert!(matches!(
1282            outcome,
1283            Err(DomainError::InvariantViolated { .. })
1284        ));
1285    }
1286
1287    /// Testimony about one's own reasoning. Nobody else has access to
1288    /// it, so nobody else may claim it.
1289    #[test]
1290    fn only_whoever_contributed_may_say_why_they_did() {
1291        let definition = definition();
1292        let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1293        let contribution = CeremonyRecordRef::contribution(agenda_item.clone(), 0);
1294        let item = CeremonyRecordRef::agenda_item(agenda_item);
1295
1296        let by_someone_else = instance.assert_reason_as(
1297            &definition,
1298            role_id("facilitator"),
1299            contribution.clone(),
1300            item.clone(),
1301            CeremonyReasonKind::ChosenBecause,
1302            "they must have thought the queue mattered",
1303            MemoryConfidence::Low,
1304            now(),
1305        );
1306        assert!(matches!(
1307            by_someone_else,
1308            Err(DomainError::InvariantViolated { .. })
1309        ));
1310
1311        instance
1312            .assert_reason_as(
1313                &definition,
1314                role_id("observer"),
1315                contribution,
1316                item,
1317                CeremonyReasonKind::ChosenBecause,
1318                "the depth graph had been flat for an hour",
1319                MemoryConfidence::High,
1320                now(),
1321            )
1322            .expect("its author may say why");
1323        assert_eq!(instance.reasons().len(), 2);
1324    }
1325
1326    /// A claim about the world, not about a mind. Anyone may make one
1327    /// and everyone may weigh it.
1328    #[test]
1329    fn any_seat_may_claim_that_one_thing_came_from_another() {
1330        let definition = definition();
1331        let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1332
1333        instance
1334            .assert_reason_as(
1335                &definition,
1336                role_id("facilitator"),
1337                CeremonyRecordRef::agenda_item(agenda_item.clone()),
1338                CeremonyRecordRef::contribution(agenda_item, 0),
1339                CeremonyReasonKind::FollowsFrom,
1340                "the item stayed open because the answer raised a new question",
1341                MemoryConfidence::Medium,
1342                now(),
1343            )
1344            .expect("a claim about the world is open to any seat");
1345
1346        let asserted = instance.reasons().last().unwrap();
1347        assert_eq!(asserted.confidence(), MemoryConfidence::Medium);
1348        assert_eq!(asserted.asserted_by(), Some(&role_id("facilitator")));
1349    }
1350
1351    /// A session knows everything it has done, so a reason may not
1352    /// cite something it never produced.
1353    #[test]
1354    fn a_reason_cannot_cite_something_that_never_happened() {
1355        let definition = definition();
1356        let (mut instance, agenda_item) = session_with_a_contribution(&definition);
1357
1358        let outcome = instance.assert_reason_as(
1359            &definition,
1360            role_id("observer"),
1361            CeremonyRecordRef::contribution(agenda_item.clone(), 7),
1362            CeremonyRecordRef::agenda_item(agenda_item),
1363            CeremonyReasonKind::FollowsFrom,
1364            "a contribution nobody made",
1365            MemoryConfidence::Low,
1366            now(),
1367        );
1368
1369        assert!(matches!(
1370            outcome,
1371            Err(DomainError::NotFound {
1372                what: "ceremony_record"
1373            })
1374        ));
1375    }
1376
1377    /// A move is recorded with the seat that fired it, so "the session
1378    /// resolved because…" has something to point at.
1379    #[test]
1380    fn a_move_is_recorded_with_whoever_made_it() {
1381        let approval =
1382            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1383        let definition = definition_with_human_guard(&approval);
1384        let mut instance = instance(&definition);
1385        instance
1386            .approve_guard(
1387                &definition,
1388                approval.name(),
1389                role_id("facilitator"),
1390                AuditActorKind::Human,
1391                now(),
1392            )
1393            .unwrap();
1394
1395        instance
1396            .apply_transition_as(
1397                &definition,
1398                &role_id("facilitator"),
1399                &trigger("approve"),
1400                datetime!(2026-06-06 12:05:00 UTC),
1401            )
1402            .unwrap();
1403
1404        let [moved] = instance.transitions() else {
1405            panic!("expected one move, got {:?}", instance.transitions());
1406        };
1407        assert_eq!(moved.trigger(), &trigger("approve"));
1408        assert_eq!(moved.from_state(), &state_id("drafting"));
1409        assert_eq!(moved.to_state(), &state_id("done"));
1410        assert_eq!(moved.applied_by(), Some(&role_id("facilitator")));
1411    }
1412
1413    /// And without one when the engine took the move itself. An
1414    /// absence, not a gap — and it is what stops testimony being
1415    /// claimed about something nobody can testify to.
1416    #[test]
1417    fn a_move_the_engine_took_names_nobody() {
1418        let approval =
1419            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1420        let definition = definition_with_human_guard(&approval);
1421        let mut instance = instance(&definition);
1422        instance
1423            .approve_guard(
1424                &definition,
1425                approval.name(),
1426                role_id("facilitator"),
1427                AuditActorKind::Human,
1428                now(),
1429            )
1430            .unwrap();
1431
1432        instance
1433            .apply_transition(&definition, &trigger("approve"), now())
1434            .unwrap();
1435
1436        assert_eq!(instance.transitions()[0].applied_by(), None);
1437    }
1438    /// What kind of party filled the seat is recorded as declared and
1439    /// never inferred.
1440    ///
1441    /// The engine knows this guard demands a human. That says one was
1442    /// required, not that one turned up — and a receipt that read
1443    /// compliance off its own requirement would assert exactly what
1444    /// nobody can demonstrate. So an agent approving a human-approval
1445    /// guard is recorded as an agent, and whether that is acceptable
1446    /// is a question for whoever reads it.
1447    #[test]
1448    fn an_approval_records_the_kind_it_was_told_not_the_one_the_guard_wanted() {
1449        let approval =
1450            CeremonyGuard::new(guard_name("human_approved"), GuardCondition::HumanApproval);
1451        let definition = definition_with_human_guard(&approval);
1452        let mut instance = instance(&definition);
1453
1454        instance
1455            .approve_guard(
1456                &definition,
1457                approval.name(),
1458                role_id("facilitator"),
1459                AuditActorKind::Agent,
1460                now(),
1461            )
1462            .unwrap();
1463
1464        let [recorded] = instance.guard_approvals() else {
1465            panic!("expected one approval");
1466        };
1467        assert_eq!(
1468            recorded.approved_by_kind(),
1469            AuditActorKind::Agent,
1470            "the guard asked for a human and an agent answered; saying otherwise \
1471             would be the engine vouching for something it cannot see"
1472        );
1473        assert!(instance.context().is_guard_approved(approval.name()));
1474    }
1475}