1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6use std::io::{Read, Write};
7use std::path::{Path, PathBuf};
8use std::process::{Child, Command, Output, Stdio};
9use std::thread;
10use std::time::{Duration, Instant};
11use thiserror::Error;
12
13use crate::protocol_machine_trace::{
14 normalize_semantic_audit, semantic_audits_equivalent, EffectTraceEvent,
15 OutputConditionTraceEvent,
16};
17use crate::runner::ChoreographyJson;
18use crate::semantic_objects::{ProtocolMachineSemanticObjects, TickedObsEvent};
19use crate::sim_reference::{
20 SimRunInput, SimRunOutput, SimTraceValidation, SimulationStructuredError,
21};
22use telltale_machine::{
23 EffectExchangeRecord, ProtocolMachineRefinementSlice, ReconfigurationEvent,
24 ReconfigurationPolicy,
25};
26
27#[path = "protocol_machine_runner_json_parsing.rs"]
28mod parsing;
29use parsing::{
30 parse_protocol_machine_run_output_strict, parse_required_valid, parse_sim_run_output,
31 parse_sim_trace_validation, parse_structured_errors, simulation_trace_payload,
32};
33
34#[derive(Debug, Error)]
36pub enum ProtocolMachineRunnerError {
37 #[error("protocol-machine runner binary not found at {0}")]
39 BinaryNotFound(PathBuf),
40 #[error("Failed to create temp file: {0}")]
42 TempFileError(#[from] std::io::Error),
43 #[error("protocol-machine runner failed with exit code {code}: {stderr}")]
45 ProcessFailed {
46 code: i32,
48 stderr: String,
50 },
51 #[error("Failed to parse protocol-machine runner output: {0}")]
53 ParseError(String),
54 #[error("protocol-machine runner operation '{operation}' timed out after {timeout_ms}ms")]
56 TimedOut {
57 operation: String,
59 timeout_ms: u64,
61 },
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ProtocolMachineRunInput {
67 #[serde(deserialize_with = "crate::schema::deserialize_schema_version")]
69 pub schema_version: String,
70 pub choreographies: Vec<ChoreographyJson>,
72 pub concurrency: u64,
74 pub max_steps: u64,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80pub struct ProtocolMachineSessionStatus {
81 #[serde(deserialize_with = "crate::schema::deserialize_schema_version")]
83 pub schema_version: String,
84 pub sid: u64,
86 pub terminal: bool,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92pub struct ProtocolMachineTraceEvent {
93 #[serde(deserialize_with = "crate::schema::deserialize_schema_version")]
95 pub schema_version: String,
96 pub kind: String,
97 pub tick: u64,
98 #[serde(default)]
99 pub session: Option<u64>,
100 #[serde(default)]
101 pub sender: Option<String>,
102 #[serde(default)]
103 pub receiver: Option<String>,
104 #[serde(default)]
105 pub label: Option<String>,
106 #[serde(default)]
107 pub role: Option<String>,
108 #[serde(default)]
109 pub target: Option<String>,
110 #[serde(default)]
111 pub permitted: Option<bool>,
112 #[serde(default)]
113 pub epoch: Option<u64>,
114 #[serde(default)]
115 pub ghost: Option<u64>,
116 #[serde(default)]
117 pub from: Option<u64>,
118 #[serde(default)]
119 pub to: Option<u64>,
120 #[serde(default)]
121 pub predicate_ref: Option<String>,
122 #[serde(default)]
123 pub witness_ref: Option<String>,
124 #[serde(default)]
125 pub output_digest: Option<String>,
126 #[serde(default)]
127 pub passed: Option<bool>,
128 #[serde(default)]
129 pub reason: Option<String>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ProtocolMachineStepState {
135 #[serde(default)]
137 pub step_index: u64,
138 #[serde(default)]
140 pub pre_state: Option<ProtocolMachineRefinementSlice>,
141 #[serde(default)]
143 pub post_state: Option<ProtocolMachineRefinementSlice>,
144 #[serde(default)]
146 pub selected_coro: Option<u64>,
147 #[serde(default)]
149 pub selected_pc: Option<u64>,
150 #[serde(default)]
152 pub selected_type: Option<Value>,
153 #[serde(default)]
155 pub exec_status: Option<String>,
156 #[serde(default)]
158 pub session_type_counts: BTreeMap<u64, u64>,
159 #[serde(default)]
161 pub buffered_message_counts: BTreeMap<u64, u64>,
162 #[serde(default)]
164 pub ready_queue: Vec<u64>,
165 #[serde(default)]
167 pub blocked: BTreeMap<u64, String>,
168 #[serde(default)]
170 pub event: Option<ProtocolMachineTraceEvent>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ProtocolMachineRunOutput {
176 #[serde(deserialize_with = "crate::schema::deserialize_schema_version")]
178 pub schema_version: String,
179 pub trace: Vec<ProtocolMachineTraceEvent>,
181 pub sessions: Vec<ProtocolMachineSessionStatus>,
183 pub steps_executed: u64,
185 pub concurrency: u64,
187 pub status: String,
189 #[serde(default)]
191 pub effect_trace: Vec<EffectTraceEvent>,
192 #[serde(default)]
194 pub effect_exchanges: Vec<EffectExchangeRecord>,
195 #[serde(default)]
197 pub output_condition_trace: Vec<OutputConditionTraceEvent>,
198 #[serde(default)]
200 pub step_states: Vec<ProtocolMachineStepState>,
201 #[serde(default)]
203 pub semantic_objects: ProtocolMachineSemanticObjects,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
208pub struct LeanStructuredError {
209 pub code: String,
210 #[serde(default)]
211 pub path: Option<String>,
212 pub message: String,
213}
214
215#[derive(Debug, Clone, Serialize)]
217pub struct TraceValidation {
218 pub valid: bool,
219 #[serde(default)]
220 pub errors: Vec<LeanStructuredError>,
221}
222
223#[derive(Debug, Clone, Serialize)]
225pub struct ComparisonResult {
226 pub equivalent: bool,
227 pub semantic_audit_equivalent: bool,
228 pub session_statuses_equivalent: bool,
229 pub semantic_handoffs_equivalent: bool,
230 pub invalidation_artifacts_equivalent: bool,
231 pub rust_semantic_audit: Vec<TickedObsEvent<ProtocolMachineTraceEvent>>,
232 pub lean_semantic_audit: Vec<TickedObsEvent<ProtocolMachineTraceEvent>>,
233 #[serde(default)]
234 pub diff: Option<Value>,
235 pub lean_output: ProtocolMachineRunOutput,
236}
237
238#[derive(Debug, Clone, Serialize)]
240pub struct InvariantVerificationResult {
241 pub valid: bool,
242 #[serde(default)]
243 pub errors: Vec<LeanStructuredError>,
244 #[serde(default)]
245 pub artifacts: Value,
246}
247
248#[derive(Debug, Clone, Serialize)]
250pub struct ReconfigurationValidationResult {
251 pub valid: bool,
252 #[serde(default)]
253 pub errors: Vec<LeanStructuredError>,
254 pub event: Option<ReconfigurationEvent>,
255}
256
257pub struct ProtocolMachineRunner {
259 binary_path: PathBuf,
260}
261
262impl ProtocolMachineRunner {
263 pub const DEFAULT_BINARY_PATH: &'static str = "lean/.lake/build/bin/protocol_machine_runner";
265 pub const VALIDATOR_BINARY_PATH: &'static str =
267 "lean/.lake/build/bin/protocol_machine_validator";
268 pub const FALLBACK_SCRIPT_PATH: &'static str = "scripts/lean/protocol-machine-runner.sh";
270 pub const VALIDATOR_FALLBACK_SCRIPT_PATH: &'static str =
272 "scripts/lean/protocol-machine-validator.sh";
273 pub const DEFAULT_TIMEOUT_MS: u64 = 300_000;
275
276 fn process_timeout() -> Duration {
277 let ms = std::env::var("TELLTALE_PROTOCOL_MACHINE_TIMEOUT_MS")
278 .ok()
279 .and_then(|raw| raw.parse::<u64>().ok())
280 .unwrap_or(Self::DEFAULT_TIMEOUT_MS);
281 Duration::from_millis(ms.max(1))
282 }
283
284 fn wait_with_timeout(
285 mut child: Child,
286 timeout: Duration,
287 operation: &str,
288 ) -> Result<Output, ProtocolMachineRunnerError> {
289 let stdout_handle = child.stdout.take().map(|mut stdout| {
290 thread::spawn(move || {
291 let mut buf = Vec::new();
292 let _ = stdout.read_to_end(&mut buf);
293 buf
294 })
295 });
296 let stderr_handle = child.stderr.take().map(|mut stderr| {
297 thread::spawn(move || {
298 let mut buf = Vec::new();
299 let _ = stderr.read_to_end(&mut buf);
300 buf
301 })
302 });
303 let start = Instant::now();
304 loop {
305 match child.try_wait()? {
307 Some(status) => {
308 let stdout = stdout_handle
309 .map(|handle| handle.join().unwrap_or_default())
310 .unwrap_or_default();
311 let stderr = stderr_handle
312 .map(|handle| handle.join().unwrap_or_default())
313 .unwrap_or_default();
314 return Ok(Output {
315 status,
316 stdout,
317 stderr,
318 });
319 }
320 None => {
321 if start.elapsed() >= timeout {
322 if let Err(err) = child.kill() {
323 eprintln!(
324 "best-effort child.kill failed during timeout handling: {err}"
325 );
326 }
327 if let Err(err) = child.wait() {
328 eprintln!(
329 "best-effort child.wait failed during timeout handling: {err}"
330 );
331 }
332 if let Some(handle) = stdout_handle {
333 let _ = handle.join();
334 }
335 if let Some(handle) = stderr_handle {
336 let _ = handle.join();
337 }
338 return Err(ProtocolMachineRunnerError::TimedOut {
339 operation: operation.to_string(),
340 timeout_ms: u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX),
341 });
342 }
343 thread::sleep(Duration::from_millis(10));
344 }
345 }
346 }
347 }
348
349 fn find_workspace_root() -> Option<PathBuf> {
350 let manifest_dir = env!("CARGO_MANIFEST_DIR");
351 let mut path = PathBuf::from(manifest_dir);
352 for _ in 0..5 {
353 if path.join("lean/.lake").is_dir() {
354 return Some(path);
355 }
356 if !path.pop() {
357 break;
358 }
359 }
360 None
361 }
362
363 fn get_binary_path() -> Option<PathBuf> {
364 Self::find_workspace_root().and_then(|root| {
365 let native = root.join(Self::DEFAULT_BINARY_PATH);
366 if native.is_file() {
367 return Some(native);
368 }
369 let fallback = root.join(Self::FALLBACK_SCRIPT_PATH);
370 if fallback.is_file() {
371 return Some(fallback);
372 }
373 None
374 })
375 }
376
377 fn uses_validator(operation: &str) -> bool {
378 matches!(
379 operation,
380 "verifyProtocolBundle" | "validateReconfigurationTransition" | "inspectCapabilityModel"
381 )
382 }
383
384 fn get_validator_path() -> Option<PathBuf> {
385 Self::find_workspace_root().and_then(|root| {
386 let native = root.join(Self::VALIDATOR_BINARY_PATH);
387 if native.is_file() {
388 return Some(native);
389 }
390 let fallback = root.join(Self::VALIDATOR_FALLBACK_SCRIPT_PATH);
391 if fallback.is_file() {
392 return Some(fallback);
393 }
394 None
395 })
396 }
397
398 pub fn new() -> Result<Self, ProtocolMachineRunnerError> {
404 match Self::get_binary_path() {
405 Some(path) => Ok(Self { binary_path: path }),
406 None => Err(ProtocolMachineRunnerError::BinaryNotFound(PathBuf::from(
407 Self::DEFAULT_BINARY_PATH,
408 ))),
409 }
410 }
411
412 pub fn with_binary_path(path: impl AsRef<Path>) -> Result<Self, ProtocolMachineRunnerError> {
418 let binary_path = PathBuf::from(path.as_ref());
419 if !binary_path.exists() || !binary_path.is_file() {
420 return Err(ProtocolMachineRunnerError::BinaryNotFound(binary_path));
421 }
422 Ok(Self { binary_path })
423 }
424
425 #[must_use]
427 pub fn try_new() -> Option<Self> {
428 Self::new().ok()
429 }
430
431 #[must_use]
433 pub fn is_available() -> bool {
434 Self::get_binary_path().is_some()
435 }
436
437 pub fn require_available() {
443 if !Self::is_available() {
444 panic!(
445 "\n\
446 ╔══════════════════════════════════════════════════════════════════╗\n\
447 ║ LEAN PROTOCOL-MACHINE RUNNER REQUIRED ║\n\
448 ╠══════════════════════════════════════════════════════════════════╣\n\
449 ║ The Lean protocol-machine runner is required but not found. ║\n\
450 ║ ║\n\
451 ║ To build Lean runners: ║\n\
452 ║ cd lean && lake build protocol_machine_runner ║\n\
453 ║ ║\n\
454 ║ Fallback launcher: ║\n\
455 ║ scripts/lean/protocol-machine-runner.sh ║\n\
456 ║ ║\n\
457 ║ Or with Nix: ║\n\
458 ║ nix develop --command bash -c \"cd lean && lake build protocol_machine_runner\" ║\n\
459 ║ ║\n\
460 ║ Expected native path: {path} \n\
461 ╚══════════════════════════════════════════════════════════════════╝\n",
462 path = Self::DEFAULT_BINARY_PATH
463 );
464 }
465 }
466
467 pub fn run(
473 &self,
474 input: &ProtocolMachineRunInput,
475 ) -> Result<ProtocolMachineRunOutput, ProtocolMachineRunnerError> {
476 crate::schema::ensure_supported_schema_version(
477 &input.schema_version,
478 "ProtocolMachineRunInput",
479 )
480 .map_err(ProtocolMachineRunnerError::ParseError)?;
481
482 let payload = serde_json::to_vec(input)
483 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))?;
484
485 let mut cmd = Command::new(&self.binary_path)
486 .stdin(Stdio::piped())
487 .stdout(Stdio::piped())
488 .stderr(Stdio::piped())
489 .spawn()
490 .map_err(ProtocolMachineRunnerError::TempFileError)?;
491
492 if let Some(mut stdin) = cmd.stdin.take() {
493 stdin.write_all(&payload)?;
494 }
495
496 let output = Self::wait_with_timeout(cmd, Self::process_timeout(), "run")?;
497
498 if !output.status.success() {
499 return Err(ProtocolMachineRunnerError::ProcessFailed {
500 code: output.status.code().unwrap_or(-1),
501 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
502 });
503 }
504
505 let out_value: Value = serde_json::from_slice(&output.stdout)
506 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))?;
507 parse_protocol_machine_run_output_strict(out_value)
508 }
509
510 pub fn run_protocol_machine(
516 &self,
517 input: &ProtocolMachineRunInput,
518 ) -> Result<ProtocolMachineRunOutput, ProtocolMachineRunnerError> {
519 self.run(input)
520 }
521
522 pub fn run_validation_operation(
528 &self,
529 operation: &str,
530 payload: &Value,
531 ) -> Result<Value, ProtocolMachineRunnerError> {
532 let input = serde_json::json!({
533 "schema_version": crate::schema::canonical_schema_version(),
534 "operation": operation,
535 "payload": payload,
536 });
537 let bytes = serde_json::to_vec(&input)
538 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))?;
539
540 let operation_binary = if Self::uses_validator(operation) {
541 Self::get_validator_path().unwrap_or_else(|| self.binary_path.clone())
542 } else {
543 self.binary_path.clone()
544 };
545
546 let mut cmd = Command::new(&operation_binary)
547 .stdin(Stdio::piped())
548 .stdout(Stdio::piped())
549 .stderr(Stdio::piped())
550 .spawn()
551 .map_err(ProtocolMachineRunnerError::TempFileError)?;
552
553 if let Some(mut stdin) = cmd.stdin.take() {
554 stdin.write_all(&bytes)?;
555 }
556
557 let output = Self::wait_with_timeout(cmd, Self::process_timeout(), operation)?;
558 if !output.status.success() {
559 return Err(ProtocolMachineRunnerError::ProcessFailed {
560 code: output.status.code().unwrap_or(-1),
561 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
562 });
563 }
564 serde_json::from_slice(&output.stdout)
565 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))
566 }
567
568 pub fn validate_trace(
574 &self,
575 choreographies: &[ChoreographyJson],
576 rust_trace: &[ProtocolMachineTraceEvent],
577 ) -> Result<TraceValidation, ProtocolMachineRunnerError> {
578 let payload = serde_json::json!({
579 "choreographies": choreographies,
580 "trace": rust_trace,
581 });
582 let response = self.run_validation_operation("validateTrace", &payload)?;
583 Ok(TraceValidation {
584 valid: parse_required_valid(&response, "validateTrace")?,
585 errors: parse_structured_errors(&response),
586 })
587 }
588
589 pub fn run_reference_simulation(
595 &self,
596 input: &SimRunInput,
597 ) -> Result<SimRunOutput, ProtocolMachineRunnerError> {
598 crate::schema::ensure_supported_schema_version(&input.schema_version, "SimRunInput")
599 .map_err(ProtocolMachineRunnerError::ParseError)?;
600 let payload = serde_json::to_value(input)
601 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))?;
602 let response = self.run_validation_operation("runSimulation", &payload)?;
603 parse_sim_run_output(response)
604 }
605
606 pub fn validate_simulation_trace(
612 &self,
613 input: &SimRunInput,
614 trace: &[ProtocolMachineTraceEvent],
615 ) -> Result<SimTraceValidation, ProtocolMachineRunnerError> {
616 let payload = simulation_trace_payload(input, trace);
617 let response = self.run_validation_operation("validateSimulationTrace", &payload)?;
618 parse_sim_trace_validation(&response)
619 }
620
621 pub fn compare_execution(
627 &self,
628 choreography: &ChoreographyJson,
629 rust_output: &ProtocolMachineRunOutput,
630 ) -> Result<ComparisonResult, ProtocolMachineRunnerError> {
631 let input = ProtocolMachineRunInput {
632 schema_version: crate::schema::canonical_schema_version(),
633 choreographies: vec![choreography.clone()],
634 concurrency: rust_output.concurrency,
635 max_steps: rust_output.steps_executed.max(1),
636 };
637 let lean_output = self.run_protocol_machine(&input)?;
638
639 let rust_ticked: Vec<TickedObsEvent<ProtocolMachineTraceEvent>> = rust_output
640 .trace
641 .iter()
642 .cloned()
643 .map(|event| TickedObsEvent {
644 tick: event.tick,
645 event,
646 })
647 .collect();
648 let lean_ticked: Vec<TickedObsEvent<ProtocolMachineTraceEvent>> = lean_output
649 .trace
650 .iter()
651 .cloned()
652 .map(|event| TickedObsEvent {
653 tick: event.tick,
654 event,
655 })
656 .collect();
657
658 let rust_normalized = normalize_semantic_audit(&rust_ticked);
659 let lean_normalized = normalize_semantic_audit(&lean_ticked);
660 let semantic_audit_equivalent = semantic_audits_equivalent(&rust_ticked, &lean_ticked);
661 let session_statuses_equivalent = rust_output.sessions == lean_output.sessions;
662 let diff = compute_execution_diff(
663 &rust_normalized,
664 &lean_normalized,
665 &rust_output.sessions,
666 &lean_output.sessions,
667 );
668 let semantic_handoffs_equivalent = rust_output.semantic_objects.semantic_handoffs
669 == lean_output.semantic_objects.semantic_handoffs;
670 let rust_invalidated_effects: Vec<_> = rust_output
671 .semantic_objects
672 .outstanding_effects
673 .iter()
674 .filter(|effect| {
675 matches!(
676 effect.status,
677 crate::semantic_objects::OutstandingEffectStatus::Invalidated
678 )
679 })
680 .map(|effect| effect.effect_id)
681 .collect();
682 let lean_invalidated_effects: Vec<_> = lean_output
683 .semantic_objects
684 .outstanding_effects
685 .iter()
686 .filter(|effect| {
687 matches!(
688 effect.status,
689 crate::semantic_objects::OutstandingEffectStatus::Invalidated
690 )
691 })
692 .map(|effect| effect.effect_id)
693 .collect();
694 let invalidation_artifacts_equivalent = rust_invalidated_effects
695 == lean_invalidated_effects
696 && rust_output.semantic_objects.transformation_obligations
697 == lean_output.semantic_objects.transformation_obligations;
698
699 Ok(ComparisonResult {
700 equivalent: semantic_audit_equivalent && session_statuses_equivalent,
701 semantic_audit_equivalent,
702 session_statuses_equivalent,
703 semantic_handoffs_equivalent,
704 invalidation_artifacts_equivalent,
705 rust_semantic_audit: rust_normalized,
706 lean_semantic_audit: lean_normalized,
707 diff,
708 lean_output,
709 })
710 }
711
712 pub fn verify_invariants(
718 &self,
719 bundle: &crate::invariants::ProtocolBundle,
720 ) -> Result<InvariantVerificationResult, ProtocolMachineRunnerError> {
721 let payload = serde_json::to_value(bundle)
722 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))?;
723 let response = self.run_validation_operation("verifyProtocolBundle", &payload)?;
724
725 Ok(InvariantVerificationResult {
726 valid: parse_required_valid(&response, "verifyProtocolBundle")?,
727 errors: parse_structured_errors(&response),
728 artifacts: response.get("artifacts").cloned().unwrap_or(Value::Null),
729 })
730 }
731
732 pub fn validate_reconfiguration_transition(
738 &self,
739 artifact_id: &str,
740 policy: &ReconfigurationPolicy,
741 starting_epoch: u64,
742 previous_members: &[String],
743 next_members: &[String],
744 ) -> Result<ReconfigurationValidationResult, ProtocolMachineRunnerError> {
745 let payload = serde_json::json!({
746 "artifact_id": artifact_id,
747 "policy": policy,
748 "starting_epoch": starting_epoch,
749 "previous_members": previous_members,
750 "next_members": next_members,
751 });
752 let response =
753 self.run_validation_operation("validateReconfigurationTransition", &payload)?;
754 let event = response
755 .get("artifacts")
756 .and_then(|artifacts| artifacts.get("event"))
757 .cloned()
758 .map(serde_json::from_value)
759 .transpose()
760 .map_err(|err| ProtocolMachineRunnerError::ParseError(err.to_string()))?;
761
762 Ok(ReconfigurationValidationResult {
763 valid: parse_required_valid(&response, "validateReconfigurationTransition")?,
764 errors: parse_structured_errors(&response),
765 event,
766 })
767 }
768
769 pub fn inspect_capability_model(
775 &self,
776 payload: &Value,
777 ) -> Result<Value, ProtocolMachineRunnerError> {
778 self.run_validation_operation("inspectCapabilityModel", payload)
779 }
780}
781
782#[must_use]
784pub fn compute_trace_diff(
785 rust_trace: &[TickedObsEvent<ProtocolMachineTraceEvent>],
786 lean_trace: &[TickedObsEvent<ProtocolMachineTraceEvent>],
787) -> Option<Value> {
788 if rust_trace == lean_trace {
789 return None;
790 }
791
792 let min_len = rust_trace.len().min(lean_trace.len());
793 for idx in 0..min_len {
794 if rust_trace[idx] != lean_trace[idx] {
795 return Some(serde_json::json!({
796 "kind": "event_mismatch",
797 "index": idx,
798 "rust": rust_trace[idx],
799 "lean": lean_trace[idx],
800 "rust_len": rust_trace.len(),
801 "lean_len": lean_trace.len(),
802 }));
803 }
804 }
805
806 Some(serde_json::json!({
807 "kind": "length_mismatch",
808 "rust_len": rust_trace.len(),
809 "lean_len": lean_trace.len(),
810 }))
811}
812
813fn compute_execution_diff(
814 rust_trace: &[TickedObsEvent<ProtocolMachineTraceEvent>],
815 lean_trace: &[TickedObsEvent<ProtocolMachineTraceEvent>],
816 rust_sessions: &[ProtocolMachineSessionStatus],
817 lean_sessions: &[ProtocolMachineSessionStatus],
818) -> Option<Value> {
819 if let Some(diff) = compute_trace_diff(rust_trace, lean_trace) {
820 return Some(diff);
821 }
822
823 if rust_sessions != lean_sessions {
824 return Some(serde_json::json!({
825 "kind": "session_status_mismatch",
826 "rust": rust_sessions,
827 "lean": lean_sessions,
828 }));
829 }
830
831 None
832}
833
834pub fn protocol_machine_input_from_values(
840 choreographies: Vec<Value>,
841 concurrency: u64,
842 max_steps: u64,
843) -> Result<ProtocolMachineRunInput, ProtocolMachineRunnerError> {
844 let mut choreos = Vec::new();
845 for value in choreographies {
846 let choreo: ChoreographyJson = serde_json::from_value(value)
847 .map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))?;
848 choreos.push(choreo);
849 }
850 Ok(ProtocolMachineRunInput {
851 schema_version: crate::schema::canonical_schema_version(),
852 choreographies: choreos,
853 concurrency,
854 max_steps,
855 })
856}
857
858pub fn output_to_json(
860 output: &ProtocolMachineRunOutput,
861) -> Result<Value, ProtocolMachineRunnerError> {
862 serde_json::to_value(output).map_err(|e| ProtocolMachineRunnerError::ParseError(e.to_string()))
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use std::process::Command;
869 use std::time::Duration;
870
871 fn trace_event(kind: &str, tick: u64, session: Option<u64>) -> ProtocolMachineTraceEvent {
872 ProtocolMachineTraceEvent {
873 schema_version: crate::schema::canonical_schema_version(),
874 kind: kind.to_string(),
875 tick,
876 session,
877 sender: None,
878 receiver: None,
879 label: None,
880 role: None,
881 target: None,
882 permitted: None,
883 epoch: None,
884 ghost: None,
885 from: None,
886 to: None,
887 predicate_ref: None,
888 witness_ref: None,
889 output_digest: None,
890 passed: None,
891 reason: None,
892 }
893 }
894
895 #[test]
896 fn compute_trace_diff_none_for_equal_traces() {
897 let trace = vec![TickedObsEvent {
898 tick: 0,
899 event: trace_event("sent", 1, Some(0)),
900 }];
901 assert!(compute_trace_diff(&trace, &trace).is_none());
902 }
903
904 #[test]
905 fn compute_trace_diff_reports_event_mismatch() {
906 let rust_trace = vec![TickedObsEvent {
907 tick: 0,
908 event: trace_event("sent", 1, Some(0)),
909 }];
910 let lean_trace = vec![TickedObsEvent {
911 tick: 0,
912 event: trace_event("received", 1, Some(0)),
913 }];
914 let diff = compute_trace_diff(&rust_trace, &lean_trace).expect("expected diff");
915 assert_eq!(diff["kind"], "event_mismatch");
916 assert_eq!(diff["index"], 0);
917 }
918
919 #[test]
920 fn parse_structured_errors_reads_codes_and_paths() {
921 let response = serde_json::json!({
922 "errors": [
923 { "code": "trace.mismatch", "path": "trace[0]", "message": "mismatch" }
924 ]
925 });
926 let errors = parse_structured_errors(&response);
927 assert_eq!(errors.len(), 1);
928 assert_eq!(errors[0].code, "trace.mismatch");
929 assert_eq!(errors[0].path.as_deref(), Some("trace[0]"));
930 }
931
932 #[test]
933 fn parse_sim_trace_validation_reads_errors_and_artifacts() {
934 let response = serde_json::json!({
935 "valid": false,
936 "errors": [
937 { "code": "sim.trace.mismatch", "path": "trace[1]", "message": "mismatch" }
938 ],
939 "artifacts": { "kind": "diff" }
940 });
941 let parsed = parse_sim_trace_validation(&response).expect("parse simulation validation");
942 assert!(!parsed.valid);
943 assert_eq!(parsed.errors.len(), 1);
944 assert_eq!(parsed.errors[0].code, "sim.trace.mismatch");
945 assert_eq!(parsed.errors[0].path.as_deref(), Some("trace[1]"));
946 assert_eq!(parsed.artifacts["kind"], "diff");
947 }
948
949 #[test]
950 fn parse_required_valid_rejects_missing_or_non_boolean() {
951 let missing = serde_json::json!({
952 "errors": []
953 });
954 let missing_err =
955 parse_required_valid(&missing, "validateTrace").expect_err("missing valid must fail");
956 assert!(matches!(
957 missing_err,
958 ProtocolMachineRunnerError::ParseError(_)
959 ));
960
961 let wrong_type = serde_json::json!({
962 "valid": "true"
963 });
964 let wrong_type_err = parse_required_valid(&wrong_type, "validateTrace")
965 .expect_err("non-boolean valid must fail");
966 assert!(matches!(
967 wrong_type_err,
968 ProtocolMachineRunnerError::ParseError(_)
969 ));
970 }
971
972 #[test]
973 fn parse_sim_run_output_checks_schema_version() {
974 let payload = serde_json::json!({
975 "schema_version": crate::schema::canonical_schema_version(),
976 "trace": [],
977 "violations": [],
978 "artifacts": {}
979 });
980 let parsed = parse_sim_run_output(payload).expect("parse sim run output");
981 assert_eq!(
982 parsed.schema_version,
983 crate::schema::canonical_schema_version()
984 );
985 }
986
987 #[test]
988 fn simulation_trace_payload_has_expected_shape() {
989 let input = SimRunInput {
990 schema_version: crate::schema::canonical_schema_version(),
991 scenario: serde_json::json!({ "kind": "unit-test" }),
992 global_type: serde_json::json!({ "tag": "end" }),
993 local_types: std::collections::BTreeMap::new(),
994 initial_states: std::collections::BTreeMap::new(),
995 };
996 let trace = vec![trace_event("sent", 1, Some(0))];
997 let payload = simulation_trace_payload(&input, &trace);
998 assert_eq!(payload["input"]["schema_version"], input.schema_version);
999 assert!(payload["trace"].is_array());
1000 assert_eq!(payload["trace"][0]["kind"], "sent");
1001 }
1002
1003 #[test]
1004 fn compute_execution_diff_rejects_session_status_order_mismatch_exactly() {
1005 let rust_statuses = vec![
1006 ProtocolMachineSessionStatus {
1007 schema_version: crate::schema::canonical_schema_version(),
1008 sid: 3,
1009 terminal: true,
1010 },
1011 ProtocolMachineSessionStatus {
1012 schema_version: crate::schema::canonical_schema_version(),
1013 sid: 1,
1014 terminal: false,
1015 },
1016 ];
1017 let lean_statuses = vec![
1018 ProtocolMachineSessionStatus {
1019 schema_version: crate::schema::canonical_schema_version(),
1020 sid: 1,
1021 terminal: false,
1022 },
1023 ProtocolMachineSessionStatus {
1024 schema_version: crate::schema::canonical_schema_version(),
1025 sid: 3,
1026 terminal: true,
1027 },
1028 ];
1029
1030 let diff = compute_execution_diff(&[], &[], &rust_statuses, &lean_statuses)
1031 .expect("exact comparison must reject session-order drift");
1032 assert_eq!(diff["kind"], "session_status_mismatch");
1033 }
1034
1035 #[test]
1036 fn wait_with_timeout_returns_timeout_error() {
1037 let child = Command::new("sh")
1038 .arg("-c")
1039 .arg("sleep 1")
1040 .spawn()
1041 .expect("spawn sleep");
1042 let result = ProtocolMachineRunner::wait_with_timeout(
1043 child,
1044 Duration::from_millis(10),
1045 "test_sleep",
1046 );
1047 assert!(matches!(
1048 result,
1049 Err(ProtocolMachineRunnerError::TimedOut { .. })
1050 ));
1051 }
1052}