Skip to main content

telltale_machine/
integration.rs

1//! First-party integration harness utilities.
2//!
3//! These helpers validate host integration behavior without changing ProtocolMachine
4//! semantics. They run on top of the canonical ProtocolMachine APIs.
5
6use crate::determinism::{replay_consistent, DeterminismMode};
7use crate::effect::{EffectHandler, RecordingEffectHandler};
8use crate::engine::{ProtocolMachine, ProtocolMachineError};
9use serde::{Deserialize, Serialize};
10use std::io::Cursor;
11
12fn encode_snapshot(machine: &ProtocolMachine) -> Result<Vec<u8>, ProtocolMachineError> {
13    let mut bytes = Vec::new();
14    ciborium::into_writer(machine, &mut bytes).map_err(|e| {
15        ProtocolMachineError::PersistenceError(format!("integration snapshot encode failed: {e}"))
16    })?;
17    Ok(bytes)
18}
19
20fn decode_snapshot(bytes: &[u8]) -> Result<ProtocolMachine, ProtocolMachineError> {
21    ciborium::from_reader(Cursor::new(bytes)).map_err(|e| {
22        ProtocolMachineError::PersistenceError(format!("integration snapshot decode failed: {e}"))
23    })
24}
25
26/// Summary from loaded protocol-machine record/replay conformance execution.
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct LoadedProtocolMachineReplayConformance {
29    /// Determinism mode used for replay consistency checks.
30    pub determinism_mode: DeterminismMode,
31    /// Profile-aware consistency outcome.
32    pub replay_consistent: bool,
33    /// Consistency outcome under `ProtocolMachineConfig.determinism_mode`.
34    pub config_mode_consistent: bool,
35    /// Exact observable trace equality.
36    pub exact_trace_match: bool,
37    /// Exact effect-trace equality.
38    pub exact_effect_trace_match: bool,
39    /// Number of recorded effect entries used for replay.
40    pub recorded_effect_count: usize,
41    /// Baseline observable event count.
42    pub baseline_event_count: usize,
43    /// Replay observable event count.
44    pub replay_event_count: usize,
45}
46
47/// Run record-and-replay conformance against a loaded protocol machine.
48///
49/// The helper snapshots the provided ProtocolMachine, executes a baseline run with
50/// `RecordingEffectHandler`, then replays the recorded effect trace from the
51/// snapshot state. The input `machine` is left in the baseline post-run state.
52///
53/// # Errors
54///
55/// Returns `ProtocolMachineError` if baseline run, replay run, or snapshot encode/decode fails.
56pub fn run_loaded_protocol_machine_record_replay_conformance(
57    machine: &mut ProtocolMachine,
58    handler: &dyn EffectHandler,
59    max_steps: usize,
60) -> Result<LoadedProtocolMachineReplayConformance, ProtocolMachineError> {
61    let snapshot = encode_snapshot(machine)?;
62
63    let recording = RecordingEffectHandler::new(handler);
64    machine.run(&recording, max_steps)?;
65
66    let recorded_effects = recording.effect_trace();
67    let baseline_trace = machine.trace().to_vec();
68    let baseline_effect_trace = machine.effect_trace().to_vec();
69    let determinism_mode = machine.config().determinism_mode;
70
71    let mut replay_vm = decode_snapshot(&snapshot)?;
72    replay_vm.run_replay(handler, &recorded_effects, max_steps)?;
73
74    let replay_trace = replay_vm.trace().to_vec();
75    let replay_effect_trace = replay_vm.effect_trace().to_vec();
76    let replay_mode_consistent = replay_consistent(
77        DeterminismMode::Replay,
78        &baseline_trace,
79        &replay_trace,
80        &baseline_effect_trace,
81        &replay_effect_trace,
82    );
83    let config_mode_consistent = replay_consistent(
84        determinism_mode,
85        &baseline_trace,
86        &replay_trace,
87        &baseline_effect_trace,
88        &replay_effect_trace,
89    );
90
91    Ok(LoadedProtocolMachineReplayConformance {
92        determinism_mode,
93        replay_consistent: replay_mode_consistent,
94        config_mode_consistent,
95        exact_trace_match: baseline_trace == replay_trace,
96        exact_effect_trace_match: baseline_effect_trace == replay_effect_trace,
97        recorded_effect_count: recorded_effects.len(),
98        baseline_event_count: baseline_trace.len(),
99        replay_event_count: replay_trace.len(),
100    })
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::coroutine::Value;
107    use crate::durable::WalSyncRequest;
108    use crate::effect::{
109        EffectFailure, EffectResult, SendDecision, SendDecisionInput, TopologyPerturbation,
110    };
111    use crate::engine::ProtocolMachineConfig;
112    use crate::loader::CodeImage;
113    use crate::output_condition::OutputConditionHint;
114    use std::collections::BTreeMap;
115    use telltale_types::{GlobalType, Label, LocalTypeR};
116
117    struct DeterministicHandler;
118
119    impl EffectHandler for DeterministicHandler {
120        fn handle_send(
121            &self,
122            _role: &str,
123            _partner: &str,
124            _label: &str,
125            _state: &[Value],
126        ) -> EffectResult<Value> {
127            EffectResult::success(Value::Nat(1))
128        }
129
130        fn send_decision(&self, input: SendDecisionInput<'_>) -> EffectResult<SendDecision> {
131            EffectResult::success(SendDecision::Deliver(input.payload.unwrap_or(Value::Unit)))
132        }
133
134        fn handle_recv(
135            &self,
136            _role: &str,
137            _partner: &str,
138            _label: &str,
139            _state: &mut Vec<Value>,
140            _payload: &Value,
141        ) -> EffectResult<()> {
142            EffectResult::success(())
143        }
144
145        fn handle_choose(
146            &self,
147            _role: &str,
148            _partner: &str,
149            labels: &[String],
150            _state: &[Value],
151        ) -> EffectResult<String> {
152            match labels.first().cloned() {
153                Some(label) => EffectResult::success(label),
154                None => EffectResult::failure(EffectFailure::invalid_input("no labels available")),
155            }
156        }
157
158        fn step(&self, _role: &str, _state: &mut Vec<Value>) -> EffectResult<()> {
159            EffectResult::success(())
160        }
161    }
162
163    struct DeterministicInternalEffectHandler;
164
165    impl EffectHandler for DeterministicInternalEffectHandler {
166        fn handle_send(
167            &self,
168            _role: &str,
169            _partner: &str,
170            _label: &str,
171            _state: &[Value],
172        ) -> EffectResult<Value> {
173            EffectResult::success(Value::Nat(1))
174        }
175
176        fn send_decision(&self, input: SendDecisionInput<'_>) -> EffectResult<SendDecision> {
177            EffectResult::success(SendDecision::Deliver(input.payload.unwrap_or(Value::Unit)))
178        }
179
180        fn handle_recv(
181            &self,
182            _role: &str,
183            _partner: &str,
184            _label: &str,
185            _state: &mut Vec<Value>,
186            _payload: &Value,
187        ) -> EffectResult<()> {
188            EffectResult::success(())
189        }
190
191        fn handle_choose(
192            &self,
193            _role: &str,
194            _partner: &str,
195            labels: &[String],
196            _state: &[Value],
197        ) -> EffectResult<String> {
198            match labels.first().cloned() {
199                Some(label) => EffectResult::success(label),
200                None => EffectResult::failure(EffectFailure::invalid_input("no labels available")),
201            }
202        }
203
204        fn step(&self, _role: &str, _state: &mut Vec<Value>) -> EffectResult<()> {
205            EffectResult::success(())
206        }
207
208        fn topology_events(&self, tick: u64) -> EffectResult<Vec<TopologyPerturbation>> {
209            let events = match tick {
210                1 => vec![TopologyPerturbation::Partition {
211                    from: "A".to_string(),
212                    to: "B".to_string(),
213                }],
214                2 => vec![TopologyPerturbation::Heal {
215                    from: "A".to_string(),
216                    to: "B".to_string(),
217                }],
218                _ => Vec::new(),
219            };
220            EffectResult::success(events)
221        }
222
223        fn output_condition_hint(
224            &self,
225            sid: usize,
226            role: &str,
227            _state: &[Value],
228        ) -> Option<OutputConditionHint> {
229            Some(OutputConditionHint {
230                predicate_ref: "machine.integration.internal_effects".to_string(),
231                witness_ref: Some(format!("sid:{sid}:role:{role}")),
232            })
233        }
234
235        fn supports_wal_sync(&self) -> bool {
236            true
237        }
238
239        fn wal_sync(&self, _sync: &WalSyncRequest) -> EffectResult<()> {
240            EffectResult::success(())
241        }
242    }
243
244    fn simple_send_recv_image() -> CodeImage {
245        let mut local_types = BTreeMap::new();
246        local_types.insert(
247            "A".to_string(),
248            LocalTypeR::Send {
249                partner: "B".into(),
250                branches: vec![(Label::new("msg"), None, LocalTypeR::End)],
251            },
252        );
253        local_types.insert(
254            "B".to_string(),
255            LocalTypeR::Recv {
256                partner: "A".into(),
257                branches: vec![(Label::new("msg"), None, LocalTypeR::End)],
258            },
259        );
260
261        let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
262        CodeImage::from_local_types(&local_types, &global)
263    }
264
265    #[test]
266    fn loaded_protocol_machine_harness_reports_replay_conformance() {
267        let image = simple_send_recv_image();
268        let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
269        machine
270            .load_choreography(&image)
271            .expect("load choreography");
272
273        let report = run_loaded_protocol_machine_record_replay_conformance(
274            &mut machine,
275            &DeterministicHandler,
276            100,
277        )
278        .expect("harness run should succeed");
279
280        assert!(report.replay_consistent);
281        assert!(report.config_mode_consistent);
282        assert!(report.exact_trace_match);
283        assert!(report.exact_effect_trace_match);
284        assert!(report.recorded_effect_count > 0);
285        assert!(report.baseline_event_count > 0);
286    }
287
288    #[test]
289    fn loaded_protocol_machine_harness_preserves_internal_effect_replay_exactness() {
290        let image = simple_send_recv_image();
291        let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
292        machine
293            .load_choreography(&image)
294            .expect("load choreography");
295
296        let report = run_loaded_protocol_machine_record_replay_conformance(
297            &mut machine,
298            &DeterministicInternalEffectHandler,
299            100,
300        )
301        .expect("harness run should succeed");
302
303        assert!(report.replay_consistent);
304        assert!(report.config_mode_consistent);
305        assert!(report.exact_trace_match);
306        assert!(report.exact_effect_trace_match);
307        assert!(report.recorded_effect_count > 0);
308    }
309}