mockforge_scenarios/
state_machine.rs

1//! Scenario state machine manager
2//!
3//! Provides functionality for loading, validating, and executing state machines
4//! from scenario manifests. Manages active state instances and real-time state tracking.
5
6use crate::error::{Result, ScenarioError};
7use crate::manifest::ScenarioManifest;
8use mockforge_core::intelligent_behavior::{
9    condition_evaluator::{ConditionError, ConditionEvaluator, ConditionResult},
10    history::HistoryManager,
11    rules::{StateMachine, StateTransition},
12    sub_scenario::SubScenario,
13    visual_layout::VisualLayout,
14};
15use serde_json::Value;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::RwLock;
19use tracing::{debug, error, info, warn};
20use uuid::Uuid;
21
22/// Active state instance for a state machine
23///
24/// Tracks the current state of a specific resource instance within a state machine.
25#[derive(Debug, Clone)]
26pub struct StateInstance {
27    /// Resource identifier (e.g., entity ID)
28    pub resource_id: String,
29
30    /// Current state
31    pub current_state: String,
32
33    /// State machine resource type
34    pub resource_type: String,
35
36    /// State history (for undo/redo and debugging)
37    pub state_history: Vec<StateHistoryEntry>,
38
39    /// Custom state data (key-value pairs)
40    pub state_data: HashMap<String, Value>,
41}
42
43/// Entry in state history
44#[derive(Debug, Clone)]
45pub struct StateHistoryEntry {
46    /// Previous state
47    pub from_state: String,
48
49    /// New state
50    pub to_state: String,
51
52    /// Timestamp of transition
53    pub timestamp: chrono::DateTime<chrono::Utc>,
54
55    /// Transition that was used
56    pub transition_id: Option<String>,
57}
58
59impl StateInstance {
60    /// Create a new state instance
61    pub fn new(
62        resource_id: impl Into<String>,
63        resource_type: impl Into<String>,
64        initial_state: impl Into<String>,
65    ) -> Self {
66        Self {
67            resource_id: resource_id.into(),
68            current_state: initial_state.into(),
69            resource_type: resource_type.into(),
70            state_history: Vec::new(),
71            state_data: HashMap::new(),
72        }
73    }
74
75    /// Transition to a new state
76    pub fn transition_to(&mut self, to_state: impl Into<String>, transition_id: Option<String>) {
77        let from_state = self.current_state.clone();
78        let to_state = to_state.into();
79
80        self.state_history.push(StateHistoryEntry {
81            from_state: from_state.clone(),
82            to_state: to_state.clone(),
83            timestamp: chrono::Utc::now(),
84            transition_id,
85        });
86
87        self.current_state = to_state;
88    }
89
90    /// Get the current state
91    pub fn current_state(&self) -> &str {
92        &self.current_state
93    }
94
95    /// Set state data
96    pub fn set_data(&mut self, key: impl Into<String>, value: Value) {
97        self.state_data.insert(key.into(), value);
98    }
99
100    /// Get state data
101    pub fn get_data(&self, key: &str) -> Option<&Value> {
102        self.state_data.get(key)
103    }
104}
105
106/// Manager for scenario state machines
107///
108/// Handles loading state machines from scenario manifests, validating them,
109/// executing state transitions, and managing active state instances.
110pub struct ScenarioStateMachineManager {
111    /// Loaded state machines by resource type
112    state_machines: Arc<RwLock<HashMap<String, StateMachine>>>,
113
114    /// Active state instances by resource ID
115    instances: Arc<RwLock<HashMap<String, StateInstance>>>,
116
117    /// Visual layouts by resource type
118    visual_layouts: Arc<RwLock<HashMap<String, VisualLayout>>>,
119
120    /// History managers for undo/redo (by resource type)
121    history_managers: Arc<RwLock<HashMap<String, HistoryManager>>>,
122}
123
124impl ScenarioStateMachineManager {
125    /// Create a new state machine manager
126    pub fn new() -> Self {
127        Self {
128            state_machines: Arc::new(RwLock::new(HashMap::new())),
129            instances: Arc::new(RwLock::new(HashMap::new())),
130            visual_layouts: Arc::new(RwLock::new(HashMap::new())),
131            history_managers: Arc::new(RwLock::new(HashMap::new())),
132        }
133    }
134
135    /// Load state machines from a scenario manifest
136    ///
137    /// Validates and loads all state machines defined in the manifest,
138    /// along with their visual layouts.
139    pub async fn load_from_manifest(&self, manifest: &ScenarioManifest) -> Result<()> {
140        info!(
141            "Loading {} state machines from scenario '{}'",
142            manifest.state_machines.len(),
143            manifest.name
144        );
145
146        let mut state_machines = self.state_machines.write().await;
147        let mut visual_layouts = self.visual_layouts.write().await;
148
149        for state_machine in &manifest.state_machines {
150            // Validate state machine
151            self.validate_state_machine(state_machine)?;
152
153            // Store state machine
154            let resource_type = state_machine.resource_type.clone();
155            state_machines.insert(resource_type.clone(), state_machine.clone());
156
157            // Store visual layout if available
158            if let Some(layout) = &state_machine.visual_layout {
159                visual_layouts.insert(resource_type.clone(), layout.clone());
160            }
161
162            // Also check state_machine_graphs for additional layouts
163            if let Some(layout) = manifest.state_machine_graphs.get(&resource_type) {
164                visual_layouts.insert(resource_type.clone(), layout.clone());
165            }
166
167            info!("Loaded state machine for resource type '{}'", resource_type);
168        }
169
170        Ok(())
171    }
172
173    /// Validate a state machine
174    ///
175    /// Checks that:
176    /// - Initial state exists in states list
177    /// - All transitions reference valid states
178    /// - Sub-scenario references are valid
179    /// - No circular dependencies in sub-scenarios
180    pub fn validate_state_machine(&self, state_machine: &StateMachine) -> Result<()> {
181        // Check initial state exists
182        if !state_machine.states.contains(&state_machine.initial_state) {
183            return Err(ScenarioError::InvalidManifest(format!(
184                "State machine '{}' has initial state '{}' that is not in states list",
185                state_machine.resource_type, state_machine.initial_state
186            )));
187        }
188
189        // Validate transitions
190        for transition in &state_machine.transitions {
191            if !state_machine.states.contains(&transition.from_state) {
192                return Err(ScenarioError::InvalidManifest(format!(
193                    "State machine '{}' has transition from invalid state '{}'",
194                    state_machine.resource_type, transition.from_state
195                )));
196            }
197
198            if !state_machine.states.contains(&transition.to_state) {
199                return Err(ScenarioError::InvalidManifest(format!(
200                    "State machine '{}' has transition to invalid state '{}'",
201                    state_machine.resource_type, transition.to_state
202                )));
203            }
204
205            // Validate sub-scenario references
206            if let Some(ref sub_scenario_id) = transition.sub_scenario_ref {
207                if state_machine.get_sub_scenario(sub_scenario_id).is_none() {
208                    return Err(ScenarioError::InvalidManifest(format!(
209                        "State machine '{}' references non-existent sub-scenario '{}'",
210                        state_machine.resource_type, sub_scenario_id
211                    )));
212                }
213            }
214        }
215
216        // Validate sub-scenarios recursively
217        for sub_scenario in &state_machine.sub_scenarios {
218            self.validate_state_machine(&sub_scenario.state_machine)?;
219        }
220
221        Ok(())
222    }
223
224    /// Get a state machine by resource type
225    pub async fn get_state_machine(&self, resource_type: &str) -> Option<StateMachine> {
226        let state_machines = self.state_machines.read().await;
227        state_machines.get(resource_type).cloned()
228    }
229
230    /// Get visual layout for a state machine
231    pub async fn get_visual_layout(&self, resource_type: &str) -> Option<VisualLayout> {
232        let layouts = self.visual_layouts.read().await;
233        layouts.get(resource_type).cloned()
234    }
235
236    /// Create a new state instance for a resource
237    ///
238    /// Initializes a new state instance with the initial state from the state machine.
239    pub async fn create_instance(
240        &self,
241        resource_id: impl Into<String>,
242        resource_type: impl Into<String>,
243    ) -> Result<()> {
244        let resource_id = resource_id.into();
245        let resource_type = resource_type.into();
246
247        // Get state machine
248        let state_machine = self.get_state_machine(&resource_type).await.ok_or_else(|| {
249            ScenarioError::InvalidManifest(format!(
250                "No state machine found for resource type '{}'",
251                resource_type
252            ))
253        })?;
254
255        // Create instance with initial state
256        let instance = StateInstance::new(
257            resource_id.clone(),
258            resource_type.clone(),
259            state_machine.initial_state.clone(),
260        );
261
262        let mut instances = self.instances.write().await;
263        instances.insert(resource_id, instance);
264
265        Ok(())
266    }
267
268    /// Get current state of a resource instance
269    pub async fn get_current_state(&self, resource_id: &str) -> Option<String> {
270        let instances = self.instances.read().await;
271        instances.get(resource_id).map(|i| i.current_state.clone())
272    }
273
274    /// Execute a state transition
275    ///
276    /// Attempts to transition a resource instance from its current state to a new state.
277    /// Validates the transition is allowed and evaluates any conditions.
278    pub async fn execute_transition(
279        &self,
280        resource_id: &str,
281        to_state: impl Into<String>,
282        context: Option<HashMap<String, Value>>,
283    ) -> Result<()> {
284        let to_state = to_state.into();
285        let mut instances = self.instances.write().await;
286
287        let instance = instances.get_mut(resource_id).ok_or_else(|| {
288            ScenarioError::InvalidManifest(format!(
289                "No state instance found for resource '{}'",
290                resource_id
291            ))
292        })?;
293
294        // Get state machine
295        let state_machine =
296            self.get_state_machine(&instance.resource_type).await.ok_or_else(|| {
297                ScenarioError::InvalidManifest(format!(
298                    "No state machine found for resource type '{}'",
299                    instance.resource_type
300                ))
301            })?;
302
303        // Find valid transition
304        let transition = state_machine
305            .transitions
306            .iter()
307            .find(|t| t.from_state == instance.current_state && t.to_state == to_state);
308
309        let transition = transition.ok_or_else(|| {
310            ScenarioError::InvalidManifest(format!(
311                "No valid transition from '{}' to '{}' for resource '{}'",
312                instance.current_state, to_state, resource_id
313            ))
314        })?;
315
316        // Evaluate condition if present
317        if let Some(ref condition_expr) = transition.condition_expression {
318            let mut evaluator = ConditionEvaluator::new();
319
320            // Add context variables
321            if let Some(ref ctx) = context {
322                for (key, value) in ctx {
323                    evaluator.set_variable(key.clone(), value.clone());
324                }
325            }
326
327            // Add instance state data
328            for (key, value) in &instance.state_data {
329                evaluator.set_variable(key.clone(), value.clone());
330            }
331
332            // Evaluate condition
333            match evaluator.evaluate(condition_expr) {
334                Ok(true) => {
335                    // Condition passed, proceed with transition
336                }
337                Ok(false) => {
338                    return Err(ScenarioError::InvalidManifest(format!(
339                        "Transition condition not met: {}",
340                        condition_expr
341                    )));
342                }
343                Err(e) => {
344                    return Err(ScenarioError::InvalidManifest(format!(
345                        "Error evaluating transition condition: {}",
346                        e
347                    )));
348                }
349            }
350        }
351
352        // Execute sub-scenario if referenced
353        if let Some(ref sub_scenario_id) = transition.sub_scenario_ref {
354            if let Some(sub_scenario) = state_machine.get_sub_scenario(sub_scenario_id) {
355                debug!("Executing sub-scenario '{}' for transition", sub_scenario_id);
356
357                // Execute sub-scenario with input/output mapping
358                match self
359                    .execute_sub_scenario(
360                        sub_scenario,
361                        &instance.state_data,
362                        &sub_scenario.state_machine.resource_type,
363                    )
364                    .await
365                {
366                    Ok(output_data) => {
367                        // Apply output mapping: copy sub-scenario outputs to parent instance
368                        for (sub_var, parent_var) in &sub_scenario.output_mapping {
369                            if let Some(value) = output_data.get(sub_var) {
370                                instance.state_data.insert(parent_var.clone(), value.clone());
371                                debug!(
372                                    "Mapped sub-scenario output '{}' to parent variable '{}'",
373                                    sub_var, parent_var
374                                );
375                            }
376                        }
377                    }
378                    Err(e) => {
379                        warn!("Sub-scenario execution failed: {}", e);
380                        // Continue with transition even if sub-scenario fails
381                        // (could be made configurable in the future)
382                    }
383                }
384            }
385        }
386
387        // Perform transition
388        instance.transition_to(
389            to_state.clone(),
390            Some(format!("{}-{}", instance.current_state, to_state)),
391        );
392
393        // Update history manager
394        let mut history_managers = self.history_managers.write().await;
395        let history = history_managers
396            .entry(instance.resource_type.clone())
397            .or_insert_with(HistoryManager::new);
398        // Note: We'd push the state machine to history here if we were tracking edits
399        // For now, we're just tracking execution state
400
401        info!(
402            "Resource '{}' transitioned from '{}' to '{}'",
403            resource_id, instance.current_state, to_state
404        );
405
406        Ok(())
407    }
408
409    /// Get all state instances
410    pub async fn list_instances(&self) -> Vec<StateInstance> {
411        let instances = self.instances.read().await;
412        instances.values().cloned().collect()
413    }
414
415    /// Get state instance by resource ID
416    pub async fn get_instance(&self, resource_id: &str) -> Option<StateInstance> {
417        let instances = self.instances.read().await;
418        instances.get(resource_id).cloned()
419    }
420
421    /// Delete a state instance
422    pub async fn delete_instance(&self, resource_id: &str) -> bool {
423        let mut instances = self.instances.write().await;
424        instances.remove(resource_id).is_some()
425    }
426
427    /// Get next possible states for a resource
428    ///
429    /// Returns all states that can be reached from the current state of the resource.
430    pub async fn get_next_states(&self, resource_id: &str) -> Result<Vec<String>> {
431        let instances = self.instances.read().await;
432        let instance = instances.get(resource_id).ok_or_else(|| {
433            ScenarioError::InvalidManifest(format!(
434                "No state instance found for resource '{}'",
435                resource_id
436            ))
437        })?;
438
439        let state_machine =
440            self.get_state_machine(&instance.resource_type).await.ok_or_else(|| {
441                ScenarioError::InvalidManifest(format!(
442                    "No state machine found for resource type '{}'",
443                    instance.resource_type
444                ))
445            })?;
446
447        Ok(state_machine.next_states(&instance.current_state))
448    }
449
450    /// Set visual layout for a state machine
451    pub async fn set_visual_layout(&self, resource_type: &str, layout: VisualLayout) {
452        let mut layouts = self.visual_layouts.write().await;
453        layouts.insert(resource_type.to_string(), layout);
454    }
455
456    /// Clear all state machines and instances
457    pub async fn clear(&self) {
458        let mut state_machines = self.state_machines.write().await;
459        let mut instances = self.instances.write().await;
460        let mut layouts = self.visual_layouts.write().await;
461        let mut history = self.history_managers.write().await;
462
463        state_machines.clear();
464        instances.clear();
465        layouts.clear();
466        history.clear();
467    }
468
469    /// Delete a state machine by resource type
470    ///
471    /// Removes the state machine and its visual layout.
472    /// Also removes all instances associated with this resource type.
473    pub async fn delete_state_machine(&self, resource_type: &str) -> bool {
474        let mut state_machines = self.state_machines.write().await;
475        let mut visual_layouts = self.visual_layouts.write().await;
476        let mut instances = self.instances.write().await;
477        let mut history_managers = self.history_managers.write().await;
478
479        // Remove state machine
480        let removed = state_machines.remove(resource_type).is_some();
481
482        // Remove visual layout
483        visual_layouts.remove(resource_type);
484
485        // Remove all instances for this resource type
486        instances.retain(|_, instance| instance.resource_type != resource_type);
487
488        // Remove history manager
489        history_managers.remove(resource_type);
490
491        if removed {
492            info!("Deleted state machine for resource type '{}'", resource_type);
493        }
494
495        removed
496    }
497
498    /// List all state machines
499    ///
500    /// Returns a list of all loaded state machines with their resource types.
501    pub async fn list_state_machines(&self) -> Vec<(String, StateMachine)> {
502        let state_machines = self.state_machines.read().await;
503        state_machines.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
504    }
505
506    /// Get all state machines and visual layouts for export
507    ///
508    /// Returns all state machines and their associated visual layouts
509    /// in a format suitable for export.
510    pub async fn export_all(&self) -> (Vec<StateMachine>, HashMap<String, VisualLayout>) {
511        let state_machines = self.state_machines.read().await;
512        let visual_layouts = self.visual_layouts.read().await;
513
514        let machines: Vec<StateMachine> = state_machines.values().cloned().collect();
515        let layouts: HashMap<String, VisualLayout> =
516            visual_layouts.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
517
518        (machines, layouts)
519    }
520
521    /// Execute a sub-scenario with input/output mapping
522    ///
523    /// Creates a nested state instance, applies input mapping, executes the sub-scenario
524    /// state machine until completion, and returns the output data for output mapping.
525    async fn execute_sub_scenario(
526        &self,
527        sub_scenario: &SubScenario,
528        parent_state_data: &HashMap<String, Value>,
529        sub_resource_type: &str,
530    ) -> Result<HashMap<String, Value>> {
531        // Generate unique resource ID for sub-scenario instance
532        let sub_instance_id = format!("sub-{}-{}", sub_scenario.id, Uuid::new_v4());
533
534        // Create sub-scenario instance with initial state
535        let mut sub_instance = StateInstance::new(
536            sub_instance_id.clone(),
537            sub_resource_type.to_string(),
538            sub_scenario.state_machine.initial_state.clone(),
539        );
540
541        // Apply input mapping: copy values from parent to sub-scenario
542        for (parent_var, sub_var) in &sub_scenario.input_mapping {
543            // Resolve parent variable value
544            // Support dot notation for nested access (e.g., "parent.status" or just "status")
545            let value = if parent_var.contains('.') {
546                // Try to resolve nested path
547                let parts: Vec<&str> = parent_var.split('.').collect();
548                if parts.len() == 2 && parts[0] == "parent" {
549                    parent_state_data.get(parts[1]).cloned()
550                } else {
551                    // Try direct lookup
552                    parent_state_data.get(parent_var).cloned()
553                }
554            } else {
555                parent_state_data.get(parent_var).cloned()
556            };
557
558            if let Some(val) = value {
559                sub_instance.set_data(sub_var.clone(), val.clone());
560                debug!(
561                    "Mapped parent variable '{}' to sub-scenario variable '{}'",
562                    parent_var, sub_var
563                );
564            } else {
565                warn!(
566                    "Parent variable '{}' not found in state data, skipping input mapping",
567                    parent_var
568                );
569            }
570        }
571
572        // Store sub-instance temporarily
573        {
574            let mut instances = self.instances.write().await;
575            instances.insert(sub_instance_id.clone(), sub_instance.clone());
576        }
577
578        // Execute sub-scenario state machine until it reaches a final state
579        // A final state is one that has no outgoing transitions
580        let mut max_iterations = 100; // Prevent infinite loops
581        let mut iteration = 0;
582
583        loop {
584            if iteration >= max_iterations {
585                warn!("Sub-scenario '{}' exceeded maximum iterations, stopping", sub_scenario.id);
586                break;
587            }
588            iteration += 1;
589
590            // Get current state
591            let current_state = sub_instance.current_state.clone();
592
593            // Check if this is a final state (no outgoing transitions)
594            let has_outgoing = sub_scenario
595                .state_machine
596                .transitions
597                .iter()
598                .any(|t| t.from_state == current_state);
599
600            if !has_outgoing {
601                debug!(
602                    "Sub-scenario '{}' reached final state '{}'",
603                    sub_scenario.id, current_state
604                );
605                break;
606            }
607
608            // Find valid transitions from current state
609            let possible_transitions: Vec<_> = sub_scenario
610                .state_machine
611                .transitions
612                .iter()
613                .filter(|t| t.from_state == current_state)
614                .collect();
615
616            if possible_transitions.is_empty() {
617                debug!(
618                    "Sub-scenario '{}' has no valid transitions from state '{}', stopping",
619                    sub_scenario.id, current_state
620                );
621                break;
622            }
623
624            // Select transition (for now, take the first valid one)
625            // In the future, this could support probability-based selection or condition evaluation
626            let selected_transition = possible_transitions[0];
627            let next_state = selected_transition.to_state.clone();
628
629            // Evaluate condition if present
630            if let Some(ref condition_expr) = selected_transition.condition_expression {
631                let mut evaluator = ConditionEvaluator::new();
632
633                // Add sub-instance state data to evaluator
634                for (key, value) in &sub_instance.state_data {
635                    evaluator.set_variable(key.clone(), value.clone());
636                }
637
638                // Evaluate condition
639                match evaluator.evaluate(condition_expr) {
640                    Ok(true) => {
641                        // Condition passed, proceed with transition
642                    }
643                    Ok(false) => {
644                        // Condition failed, try next transition or stop
645                        debug!(
646                            "Sub-scenario transition condition not met: {}, trying next transition",
647                            condition_expr
648                        );
649                        if possible_transitions.len() > 1 {
650                            // Try next transition
651                            let next_transition = possible_transitions[1];
652                            let next_state = next_transition.to_state.clone();
653                            sub_instance.transition_to(next_state, None);
654                        } else {
655                            // No more transitions, stop
656                            break;
657                        }
658                        continue;
659                    }
660                    Err(e) => {
661                        warn!(
662                            "Error evaluating sub-scenario transition condition: {}, stopping",
663                            e
664                        );
665                        break;
666                    }
667                }
668            }
669
670            // Perform transition
671            sub_instance.transition_to(next_state.clone(), None);
672            debug!(
673                "Sub-scenario '{}' transitioned from '{}' to '{}'",
674                sub_scenario.id, current_state, next_state
675            );
676
677            // Update stored instance
678            {
679                let mut instances = self.instances.write().await;
680                if let Some(stored) = instances.get_mut(&sub_instance_id) {
681                    *stored = sub_instance.clone();
682                }
683            }
684        }
685
686        // Get final state data from sub-instance
687        let output_data = sub_instance.state_data.clone();
688
689        // Clean up sub-instance
690        {
691            let mut instances = self.instances.write().await;
692            instances.remove(&sub_instance_id);
693        }
694
695        info!(
696            "Sub-scenario '{}' completed after {} iterations, final state: '{}'",
697            sub_scenario.id, iteration, sub_instance.current_state
698        );
699
700        Ok(output_data)
701    }
702}
703
704impl Default for ScenarioStateMachineManager {
705    fn default() -> Self {
706        Self::new()
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use mockforge_core::intelligent_behavior::rules::{StateMachine, StateTransition};
714
715    fn create_test_state_machine() -> StateMachine {
716        StateMachine::new(
717            "order",
718            vec![
719                "pending".to_string(),
720                "processing".to_string(),
721                "shipped".to_string(),
722            ],
723            "pending",
724        )
725        .add_transition(StateTransition::new("pending", "processing"))
726        .add_transition(StateTransition::new("processing", "shipped"))
727    }
728
729    #[tokio::test]
730    async fn test_load_state_machine() {
731        let manager = ScenarioStateMachineManager::new();
732        let mut manifest = ScenarioManifest::new(
733            "test".to_string(),
734            "1.0.0".to_string(),
735            "Test".to_string(),
736            "Test scenario".to_string(),
737        );
738        manifest.state_machines.push(create_test_state_machine());
739
740        let result = manager.load_from_manifest(&manifest).await;
741        assert!(result.is_ok());
742
743        let state_machine = manager.get_state_machine("order").await;
744        assert!(state_machine.is_some());
745        assert_eq!(state_machine.unwrap().resource_type, "order");
746    }
747
748    #[tokio::test]
749    async fn test_create_and_transition() {
750        let manager = ScenarioStateMachineManager::new();
751        let mut manifest = ScenarioManifest::new(
752            "test".to_string(),
753            "1.0.0".to_string(),
754            "Test".to_string(),
755            "Test scenario".to_string(),
756        );
757        manifest.state_machines.push(create_test_state_machine());
758
759        manager.load_from_manifest(&manifest).await.unwrap();
760        manager.create_instance("order-1", "order").await.unwrap();
761
762        let state = manager.get_current_state("order-1").await;
763        assert_eq!(state, Some("pending".to_string()));
764
765        manager.execute_transition("order-1", "processing", None).await.unwrap();
766        let state = manager.get_current_state("order-1").await;
767        assert_eq!(state, Some("processing".to_string()));
768    }
769
770    #[tokio::test]
771    async fn test_conditional_transition() {
772        let manager = ScenarioStateMachineManager::new();
773        let state_machine = StateMachine::new(
774            "order",
775            vec![
776                "pending".to_string(),
777                "approved".to_string(),
778                "rejected".to_string(),
779            ],
780            "pending",
781        )
782        .add_transition(
783            StateTransition::new("pending", "approved").with_condition_expression("amount > 100"),
784        )
785        .add_transition(
786            StateTransition::new("pending", "rejected").with_condition_expression("amount <= 100"),
787        );
788
789        let mut manifest = ScenarioManifest::new(
790            "test".to_string(),
791            "1.0.0".to_string(),
792            "Test".to_string(),
793            "Test scenario".to_string(),
794        );
795        manifest.state_machines.push(state_machine);
796
797        manager.load_from_manifest(&manifest).await.unwrap();
798        manager.create_instance("order-1", "order").await.unwrap();
799
800        // Test with condition that passes
801        let mut context = HashMap::new();
802        context.insert("amount".to_string(), Value::Number(serde_json::Number::from(150)));
803        manager.execute_transition("order-1", "approved", Some(context)).await.unwrap();
804        assert_eq!(manager.get_current_state("order-1").await, Some("approved".to_string()));
805    }
806}