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