Skip to main content

telltale_runtime/effects/handlers/
recording.rs

1// Recording effect handler for testing
2//
3// Captures all choreographic effects for verification and testing.
4// Does not produce actual values - use for protocol structure testing only.
5
6use async_trait::async_trait;
7use serde::{de::DeserializeOwned, Serialize};
8use std::time::Duration;
9
10use crate::effects::contract::{
11    DeliveryModel, DocumentedHandlerContract, ExtensionDispatchContract, ExtensionDispatchMode,
12    HandlerContractProfile, HandlerContractTier, ProtocolSemanticContract, RetryPolicy,
13    TimeoutPolicy, TransportPolicyContract,
14};
15use crate::effects::{ChoreoHandler, ChoreoResult, ChoreographyError, RoleId};
16
17/// Recording handler for testing - captures all effects for verification
18#[derive(Clone)]
19pub struct RecordingHandler<R: RoleId> {
20    pub events: std::sync::Arc<std::sync::Mutex<Vec<RecordedEvent<R>>>>,
21    role: R,
22}
23
24#[derive(Debug, Clone)]
25pub enum RecordedEvent<R: RoleId> {
26    Send { from: R, to: R, msg_type: String },
27    Recv { from: R, to: R, msg_type: String },
28    Choose { at: R, label: <R as RoleId>::Label },
29    Offer { from: R, to: R },
30}
31
32impl<R: RoleId> RecordingHandler<R> {
33    pub fn new(role: R) -> Self {
34        Self {
35            events: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
36            role,
37        }
38    }
39
40    pub fn events(&self) -> Vec<RecordedEvent<R>> {
41        self.events
42            .lock()
43            .unwrap_or_else(std::sync::PoisonError::into_inner)
44            .clone()
45    }
46
47    pub fn clear(&self) {
48        self.events
49            .lock()
50            .unwrap_or_else(std::sync::PoisonError::into_inner)
51            .clear();
52    }
53}
54
55impl<R: RoleId> DocumentedHandlerContract for RecordingHandler<R> {
56    fn contract_profile() -> HandlerContractProfile {
57        HandlerContractProfile {
58            handler_name: std::any::type_name::<Self>(),
59            tier: HandlerContractTier::ObservationalHarness,
60            semantics: ProtocolSemanticContract {
61                typed_send_recv_roundtrip: false,
62                exact_choice_label_preservation: true,
63                fail_closed_transport_errors: true,
64                timeouts_scoped_to_enforcing_role: true,
65                deterministic_for_regression: true,
66                can_materialize_values: false,
67            },
68            transport: TransportPolicyContract {
69                delivery_model: DeliveryModel::ScriptedHarness,
70                retry_policy: RetryPolicy::None,
71                timeout_policy: TimeoutPolicy::EnforcingRoleOnly,
72            },
73            extension_dispatch: ExtensionDispatchContract {
74                mode: ExtensionDispatchMode::Unsupported,
75                fail_closed_when_unregistered: false,
76                type_exact_before_side_effects: false,
77            },
78            notes: vec![
79                "captures exact operation order for regression use",
80                "recv/offer intentionally fail closed after recording the attempted effect",
81            ],
82        }
83    }
84}
85
86#[async_trait]
87impl<R: RoleId + 'static> ChoreoHandler for RecordingHandler<R> {
88    type Role = R;
89    type Endpoint = ();
90
91    async fn send<M: Serialize + Send + Sync>(
92        &mut self,
93        _ep: &mut Self::Endpoint,
94        to: Self::Role,
95        _msg: &M,
96    ) -> ChoreoResult<()> {
97        self.events
98            .lock()
99            .unwrap_or_else(std::sync::PoisonError::into_inner)
100            .push(RecordedEvent::Send {
101                from: self.role,
102                to,
103                msg_type: std::any::type_name::<M>().to_string(),
104            });
105        Ok(())
106    }
107
108    async fn recv<M: DeserializeOwned + Send>(
109        &mut self,
110        _ep: &mut Self::Endpoint,
111        from: Self::Role,
112    ) -> ChoreoResult<M> {
113        self.events
114            .lock()
115            .unwrap_or_else(std::sync::PoisonError::into_inner)
116            .push(RecordedEvent::Recv {
117                from,
118                to: self.role,
119                msg_type: std::any::type_name::<M>().to_string(),
120            });
121        Err(ChoreographyError::Transport(
122            "RecordingHandler cannot produce values".into(),
123        ))
124    }
125
126    async fn choose(
127        &mut self,
128        _ep: &mut Self::Endpoint,
129        at: Self::Role,
130        label: <Self::Role as RoleId>::Label,
131    ) -> ChoreoResult<()> {
132        self.events
133            .lock()
134            .unwrap_or_else(std::sync::PoisonError::into_inner)
135            .push(RecordedEvent::Choose { at, label });
136        Ok(())
137    }
138
139    async fn offer(
140        &mut self,
141        _ep: &mut Self::Endpoint,
142        from: Self::Role,
143    ) -> ChoreoResult<<Self::Role as RoleId>::Label> {
144        self.events
145            .lock()
146            .unwrap_or_else(std::sync::PoisonError::into_inner)
147            .push(RecordedEvent::Offer {
148                from,
149                to: self.role,
150            });
151        Err(ChoreographyError::Transport(
152            "RecordingHandler cannot produce labels".into(),
153        ))
154    }
155
156    async fn with_timeout<F, T>(
157        &mut self,
158        _ep: &mut Self::Endpoint,
159        _at: Self::Role,
160        _dur: Duration,
161        body: F,
162    ) -> ChoreoResult<T>
163    where
164        F: std::future::Future<Output = ChoreoResult<T>> + Send,
165    {
166        body.await
167    }
168}