Skip to main content

jugar_probar/playbook/
schema.rs

1//! Playbook YAML schema types for state machine testing.
2//!
3//! Implements SCXML-inspired state definitions with transition-based assertions.
4//! Reference: W3C SCXML Specification <https://www.w3.org/TR/scxml/>
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Root playbook configuration.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Playbook {
12    /// Schema version (must be "1.0")
13    pub version: String,
14    /// Playbook name
15    #[serde(default)]
16    pub name: String,
17    /// Playbook description
18    #[serde(default)]
19    pub description: String,
20    /// State machine definition
21    pub machine: StateMachine,
22    /// Performance budget constraints
23    #[serde(default)]
24    pub performance: PerformanceBudget,
25    /// Playbook execution steps
26    #[serde(default)]
27    pub playbook: Option<PlaybookSteps>,
28    /// Assertions to verify
29    #[serde(default)]
30    pub assertions: Option<PlaybookAssertions>,
31    /// Falsification protocol
32    #[serde(default)]
33    pub falsification: Option<FalsificationConfig>,
34    /// Optional metadata
35    #[serde(default)]
36    pub metadata: HashMap<String, String>,
37}
38
39/// State machine definition following SCXML semantics.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct StateMachine {
42    /// Unique identifier for the machine
43    pub id: String,
44    /// Initial state ID (must exist in states)
45    pub initial: String,
46    /// State definitions keyed by ID
47    pub states: HashMap<String, State>,
48    /// Transition definitions
49    pub transitions: Vec<Transition>,
50    /// Forbidden transitions (must never occur)
51    #[serde(default)]
52    pub forbidden: Vec<ForbiddenTransition>,
53    /// Global performance constraints
54    #[serde(default)]
55    pub performance: Option<PerformanceBudget>,
56}
57
58/// Forbidden transition that must never occur.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ForbiddenTransition {
61    /// Source state ID
62    pub from: String,
63    /// Target state ID
64    pub to: String,
65    /// Reason why this transition is forbidden
66    #[serde(default)]
67    pub reason: String,
68}
69
70/// Individual state definition.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct State {
73    /// State identifier (must be unique)
74    pub id: String,
75    /// Human-readable description
76    #[serde(default)]
77    pub description: String,
78    /// Entry actions executed when entering this state
79    #[serde(default)]
80    pub on_entry: Vec<Action>,
81    /// Exit actions executed when leaving this state
82    #[serde(default)]
83    pub on_exit: Vec<Action>,
84    /// Invariant conditions that must hold while in this state
85    #[serde(default)]
86    pub invariants: Vec<Invariant>,
87    /// Whether this is a final (accepting) state
88    #[serde(default)]
89    pub final_state: bool,
90}
91
92/// State transition definition.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct Transition {
95    /// Unique transition identifier
96    pub id: String,
97    /// Source state ID
98    pub from: String,
99    /// Target state ID
100    pub to: String,
101    /// Event that triggers this transition
102    pub event: String,
103    /// Guard condition (optional)
104    #[serde(default)]
105    pub guard: Option<String>,
106    /// Actions to execute during transition
107    #[serde(default)]
108    pub actions: Vec<Action>,
109    /// Expected assertions after transition
110    #[serde(default)]
111    pub assertions: Vec<Assertion>,
112}
113
114/// Action to execute during state entry, exit, or transition.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(tag = "type")]
117pub enum Action {
118    /// Click on an element
119    #[serde(rename = "click")]
120    Click { selector: String },
121    /// Type text into an element
122    #[serde(rename = "type")]
123    Type { selector: String, text: String },
124    /// Wait for a condition
125    #[serde(rename = "wait")]
126    Wait { condition: WaitCondition },
127    /// Navigate to URL
128    #[serde(rename = "navigate")]
129    Navigate { url: String },
130    /// Execute custom JavaScript
131    #[serde(rename = "script")]
132    Script { code: String },
133    /// Take screenshot
134    #[serde(rename = "screenshot")]
135    Screenshot { name: String },
136}
137
138/// Wait condition types.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(tag = "type")]
141pub enum WaitCondition {
142    /// Wait for element to be visible
143    #[serde(rename = "visible")]
144    Visible { selector: String },
145    /// Wait for element to be hidden
146    #[serde(rename = "hidden")]
147    Hidden { selector: String },
148    /// Wait for fixed duration (ms)
149    #[serde(rename = "duration")]
150    Duration { ms: u64 },
151    /// Wait for network idle
152    #[serde(rename = "network_idle")]
153    NetworkIdle,
154    /// Wait for custom condition
155    #[serde(rename = "condition")]
156    Condition { expression: String },
157}
158
159/// Assertion to verify after transition.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(tag = "type")]
162pub enum Assertion {
163    /// Element exists in DOM
164    #[serde(rename = "element_exists")]
165    ElementExists { selector: String },
166    /// Element has specific text
167    #[serde(rename = "text_equals")]
168    TextEquals { selector: String, expected: String },
169    /// Element has text containing substring
170    #[serde(rename = "text_contains")]
171    TextContains { selector: String, substring: String },
172    /// Element has specific attribute value
173    #[serde(rename = "attribute_equals")]
174    AttributeEquals {
175        selector: String,
176        attribute: String,
177        expected: String,
178    },
179    /// URL matches pattern
180    #[serde(rename = "url_matches")]
181    UrlMatches { pattern: String },
182    /// Custom JavaScript assertion
183    #[serde(rename = "script")]
184    Script { expression: String },
185}
186
187/// Invariant condition that must hold while in a state.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct Invariant {
190    /// Human-readable description
191    pub description: String,
192    /// Condition expression
193    pub condition: String,
194    /// Severity if violated
195    #[serde(default)]
196    pub severity: InvariantSeverity,
197}
198
199/// Severity level for invariant violations.
200#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
201#[serde(rename_all = "lowercase")]
202pub enum InvariantSeverity {
203    /// Warning only, test continues
204    Warning,
205    /// Error, test fails
206    #[default]
207    Error,
208    /// Critical, test aborts immediately
209    Critical,
210}
211
212/// Performance budget constraints.
213#[derive(Debug, Clone, Default, Serialize, Deserialize)]
214pub struct PerformanceBudget {
215    /// Maximum time per transition (ms)
216    #[serde(default)]
217    pub max_transition_time_ms: Option<u64>,
218    /// Maximum total playbook time (ms)
219    #[serde(default)]
220    pub max_total_time_ms: Option<u64>,
221    /// Maximum memory usage (bytes)
222    #[serde(default)]
223    pub max_memory_bytes: Option<u64>,
224    /// Complexity class constraint
225    #[serde(default)]
226    pub complexity_class: Option<ComplexityClass>,
227}
228
229/// Expected algorithmic complexity class.
230#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
231pub enum ComplexityClass {
232    /// O(1) - constant time
233    #[serde(rename = "O(1)")]
234    Constant,
235    /// O(log n) - logarithmic
236    #[serde(rename = "O(log n)")]
237    Logarithmic,
238    /// O(n) - linear
239    #[serde(rename = "O(n)")]
240    Linear,
241    /// O(n log n) - linearithmic
242    #[serde(rename = "O(n log n)")]
243    Linearithmic,
244    /// O(n^2) - quadratic
245    #[serde(rename = "O(n^2)")]
246    Quadratic,
247}
248
249/// Playbook execution steps (setup, steps, teardown).
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct PlaybookSteps {
252    /// Setup actions executed before steps
253    #[serde(default)]
254    pub setup: Vec<PlaybookAction>,
255    /// Ordered execution steps
256    #[serde(default)]
257    pub steps: Vec<PlaybookStep>,
258    /// Teardown actions executed after steps (even on failure)
259    #[serde(default)]
260    pub teardown: Vec<PlaybookAction>,
261}
262
263/// Single playbook action.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct PlaybookAction {
266    /// Action to execute
267    pub action: ActionSpec,
268    /// Description of the action
269    #[serde(default)]
270    pub description: String,
271    /// Whether to ignore errors
272    #[serde(default)]
273    pub ignore_errors: bool,
274}
275
276/// Action specification (wasm call or other).
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ActionSpec {
279    /// WASM function to call
280    #[serde(default)]
281    pub wasm: Option<String>,
282    /// Arguments to pass
283    #[serde(default)]
284    pub args: Vec<String>,
285}
286
287/// Single execution step.
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct PlaybookStep {
290    /// Step name
291    pub name: String,
292    /// Transitions to execute in order
293    #[serde(default)]
294    pub transitions: Vec<String>,
295    /// Timeout for this step
296    #[serde(default)]
297    pub timeout: Option<String>,
298    /// Variables to capture after step
299    #[serde(default)]
300    pub capture: Vec<VariableCapture>,
301}
302
303/// Variable capture specification.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct VariableCapture {
306    /// Variable name to store result
307    pub var: String,
308    /// Expression to evaluate
309    pub from: String,
310}
311
312/// Playbook assertions configuration.
313#[derive(Debug, Clone, Default, Serialize, Deserialize)]
314pub struct PlaybookAssertions {
315    /// Expected state path
316    #[serde(default)]
317    pub path: Option<PathAssertion>,
318    /// Output assertions
319    #[serde(default)]
320    pub output: Vec<OutputAssertion>,
321    /// Complexity assertion
322    #[serde(default)]
323    pub complexity: Option<ComplexityAssertion>,
324}
325
326/// Path assertion - expected state sequence.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct PathAssertion {
329    /// Expected sequence of state IDs
330    pub expected: Vec<String>,
331}
332
333/// Output assertion on captured variables.
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct OutputAssertion {
336    /// Variable name
337    pub var: String,
338    /// Assert not empty
339    #[serde(default)]
340    pub not_empty: Option<bool>,
341    /// Assert matches regex
342    #[serde(default)]
343    pub matches: Option<String>,
344    /// Assert less than value
345    #[serde(default)]
346    pub less_than: Option<i64>,
347    /// Assert greater than value
348    #[serde(default)]
349    pub greater_than: Option<i64>,
350    /// Assert equals value
351    #[serde(default)]
352    pub equals: Option<String>,
353}
354
355/// Complexity assertion for O(n) verification.
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct ComplexityAssertion {
358    /// Operation being measured
359    pub operation: String,
360    /// Variable containing measurement
361    pub measure: String,
362    /// Variable containing input size
363    pub input_var: String,
364    /// Expected complexity class
365    pub expected: ComplexityClass,
366    /// Allowed tolerance (0.0 - 1.0)
367    #[serde(default)]
368    pub tolerance: f64,
369    /// Sample sizes for measurement
370    #[serde(default)]
371    pub sample_sizes: Vec<usize>,
372}
373
374/// Falsification protocol configuration.
375#[derive(Debug, Clone, Default, Serialize, Deserialize)]
376pub struct FalsificationConfig {
377    /// Mutation definitions
378    #[serde(default)]
379    pub mutations: Vec<MutationDef>,
380}
381
382/// Single mutation definition for falsification testing.
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct MutationDef {
385    /// Mutation identifier
386    pub id: String,
387    /// Description of the mutation
388    #[serde(default)]
389    pub description: String,
390    /// Mutation action
391    pub mutate: String,
392    /// Expected failure message
393    pub expected_failure: String,
394}
395
396impl Playbook {
397    /// Parse a playbook from YAML string.
398    ///
399    /// # Errors
400    /// Returns error if YAML is invalid or schema validation fails.
401    pub fn from_yaml(yaml: &str) -> Result<Self, PlaybookError> {
402        let playbook: Playbook =
403            serde_yaml_ng::from_str(yaml).map_err(|e| PlaybookError::ParseError(e.to_string()))?;
404        playbook.validate()?;
405        Ok(playbook)
406    }
407
408    /// Validate the playbook structure.
409    fn validate(&self) -> Result<(), PlaybookError> {
410        // Validate version
411        if self.version != "1.0" {
412            return Err(PlaybookError::InvalidVersion(self.version.clone()));
413        }
414
415        // Validate initial state exists
416        if !self.machine.states.contains_key(&self.machine.initial) {
417            return Err(PlaybookError::InvalidInitialState(
418                self.machine.initial.clone(),
419            ));
420        }
421
422        // Validate all transition sources and targets exist
423        for transition in &self.machine.transitions {
424            if !self.machine.states.contains_key(&transition.from) {
425                return Err(PlaybookError::InvalidTransitionSource {
426                    transition_id: transition.id.clone(),
427                    state_id: transition.from.clone(),
428                });
429            }
430            if !self.machine.states.contains_key(&transition.to) {
431                return Err(PlaybookError::InvalidTransitionTarget {
432                    transition_id: transition.id.clone(),
433                    state_id: transition.to.clone(),
434                });
435            }
436        }
437
438        // Check for duplicate state IDs (HashMap handles this, but explicit check)
439        let state_ids: Vec<_> = self.machine.states.keys().collect();
440        let unique_ids: std::collections::HashSet<_> = state_ids.iter().collect();
441        if state_ids.len() != unique_ids.len() {
442            return Err(PlaybookError::DuplicateStateIds);
443        }
444
445        // Check for duplicate transition IDs
446        let transition_ids: Vec<_> = self.machine.transitions.iter().map(|t| &t.id).collect();
447        let unique_transition_ids: std::collections::HashSet<_> = transition_ids.iter().collect();
448        if transition_ids.len() != unique_transition_ids.len() {
449            return Err(PlaybookError::DuplicateTransitionIds);
450        }
451
452        // Validate no empty states or transitions
453        if self.machine.states.is_empty() {
454            return Err(PlaybookError::EmptyStates);
455        }
456        if self.machine.transitions.is_empty() {
457            return Err(PlaybookError::EmptyTransitions);
458        }
459
460        Ok(())
461    }
462}
463
464/// Errors that can occur during playbook parsing and validation.
465#[derive(Debug, Clone, thiserror::Error)]
466pub enum PlaybookError {
467    #[error("Failed to parse YAML: {0}")]
468    ParseError(String),
469
470    #[error("Invalid version '{0}', expected '1.0'")]
471    InvalidVersion(String),
472
473    #[error("Initial state '{0}' does not exist")]
474    InvalidInitialState(String),
475
476    #[error("Transition '{transition_id}' references non-existent source state '{state_id}'")]
477    InvalidTransitionSource {
478        transition_id: String,
479        state_id: String,
480    },
481
482    #[error("Transition '{transition_id}' references non-existent target state '{state_id}'")]
483    InvalidTransitionTarget {
484        transition_id: String,
485        state_id: String,
486    },
487
488    #[error("Duplicate state IDs detected")]
489    DuplicateStateIds,
490
491    #[error("Duplicate transition IDs detected")]
492    DuplicateTransitionIds,
493
494    #[error("States cannot be empty")]
495    EmptyStates,
496
497    #[error("Transitions cannot be empty")]
498    EmptyTransitions,
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    const VALID_PLAYBOOK: &str = r##"
506version: "1.0"
507machine:
508  id: "login_flow"
509  initial: "logged_out"
510  states:
511    logged_out:
512      id: "logged_out"
513      description: "User is not authenticated"
514      invariants:
515        - description: "Login button visible"
516          condition: "document.querySelector('#login-btn') !== null"
517    logged_in:
518      id: "logged_in"
519      description: "User is authenticated"
520      final_state: true
521  transitions:
522    - id: "t1"
523      from: "logged_out"
524      to: "logged_in"
525      event: "login_success"
526      assertions:
527        - type: element_exists
528          selector: "#welcome-message"
529"##;
530
531    #[test]
532    fn test_parse_valid_playbook() {
533        let playbook = Playbook::from_yaml(VALID_PLAYBOOK).expect("Should parse valid playbook");
534        assert_eq!(playbook.version, "1.0");
535        assert_eq!(playbook.machine.id, "login_flow");
536        assert_eq!(playbook.machine.initial, "logged_out");
537        assert_eq!(playbook.machine.states.len(), 2);
538        assert_eq!(playbook.machine.transitions.len(), 1);
539    }
540
541    #[test]
542    fn test_reject_invalid_version() {
543        let yaml = VALID_PLAYBOOK.replace("version: \"1.0\"", "version: \"2.0\"");
544        let result = Playbook::from_yaml(&yaml);
545        assert!(matches!(result, Err(PlaybookError::InvalidVersion(_))));
546    }
547
548    #[test]
549    fn test_reject_invalid_initial_state() {
550        let yaml = VALID_PLAYBOOK.replace("initial: \"logged_out\"", "initial: \"nonexistent\"");
551        let result = Playbook::from_yaml(&yaml);
552        assert!(matches!(result, Err(PlaybookError::InvalidInitialState(_))));
553    }
554
555    #[test]
556    fn test_reject_invalid_transition_source() {
557        let yaml = VALID_PLAYBOOK.replace("from: \"logged_out\"", "from: \"nonexistent\"");
558        let result = Playbook::from_yaml(&yaml);
559        assert!(matches!(
560            result,
561            Err(PlaybookError::InvalidTransitionSource { .. })
562        ));
563    }
564
565    #[test]
566    fn test_reject_invalid_transition_target() {
567        let yaml = VALID_PLAYBOOK.replace("to: \"logged_in\"", "to: \"nonexistent\"");
568        let result = Playbook::from_yaml(&yaml);
569        assert!(matches!(
570            result,
571            Err(PlaybookError::InvalidTransitionTarget { .. })
572        ));
573    }
574
575    #[test]
576    fn test_reject_empty_states() {
577        let yaml = r#"
578version: "1.0"
579machine:
580  id: "test"
581  initial: "start"
582  states: {}
583  transitions:
584    - id: "t1"
585      from: "start"
586      to: "end"
587      event: "go"
588"#;
589        let result = Playbook::from_yaml(yaml);
590        // Empty states causes initial state validation to fail first
591        assert!(result.is_err());
592        // Could be EmptyStates or InvalidInitialState depending on validation order
593        assert!(matches!(
594            result,
595            Err(PlaybookError::EmptyStates | PlaybookError::InvalidInitialState(_))
596        ));
597    }
598
599    #[test]
600    fn test_reject_empty_transitions() {
601        let yaml = r#"
602version: "1.0"
603machine:
604  id: "test"
605  initial: "start"
606  states:
607    start:
608      id: "start"
609  transitions: []
610"#;
611        let result = Playbook::from_yaml(yaml);
612        assert!(matches!(result, Err(PlaybookError::EmptyTransitions)));
613    }
614
615    // === Additional tests for improved coverage ===
616
617    #[test]
618    fn test_parse_error_invalid_yaml() {
619        let yaml = "this is not: valid: yaml: {{{{";
620        let result = Playbook::from_yaml(yaml);
621        assert!(matches!(result, Err(PlaybookError::ParseError(_))));
622    }
623
624    #[test]
625    fn test_parse_error_missing_required_field() {
626        let yaml = r#"
627version: "1.0"
628"#;
629        let result = Playbook::from_yaml(yaml);
630        assert!(matches!(result, Err(PlaybookError::ParseError(_))));
631    }
632
633    #[test]
634    fn test_duplicate_transition_ids() {
635        let yaml = r#"
636version: "1.0"
637machine:
638  id: "test"
639  initial: "a"
640  states:
641    a:
642      id: "a"
643    b:
644      id: "b"
645  transitions:
646    - id: "t1"
647      from: "a"
648      to: "b"
649      event: "go"
650    - id: "t1"
651      from: "b"
652      to: "a"
653      event: "back"
654"#;
655        let result = Playbook::from_yaml(yaml);
656        assert!(matches!(result, Err(PlaybookError::DuplicateTransitionIds)));
657    }
658
659    #[test]
660    fn test_playbook_error_display() {
661        // Test all error Display implementations
662        let err = PlaybookError::ParseError("test error".to_string());
663        assert!(err.to_string().contains("Failed to parse YAML"));
664
665        let err = PlaybookError::InvalidVersion("2.0".to_string());
666        assert!(err.to_string().contains("Invalid version '2.0'"));
667
668        let err = PlaybookError::InvalidInitialState("missing".to_string());
669        assert!(err.to_string().contains("Initial state 'missing'"));
670
671        let err = PlaybookError::InvalidTransitionSource {
672            transition_id: "t1".to_string(),
673            state_id: "missing".to_string(),
674        };
675        assert!(err.to_string().contains("Transition 't1'"));
676        assert!(err.to_string().contains("source state 'missing'"));
677
678        let err = PlaybookError::InvalidTransitionTarget {
679            transition_id: "t2".to_string(),
680            state_id: "gone".to_string(),
681        };
682        assert!(err.to_string().contains("Transition 't2'"));
683        assert!(err.to_string().contains("target state 'gone'"));
684
685        let err = PlaybookError::DuplicateStateIds;
686        assert!(err.to_string().contains("Duplicate state IDs"));
687
688        let err = PlaybookError::DuplicateTransitionIds;
689        assert!(err.to_string().contains("Duplicate transition IDs"));
690
691        let err = PlaybookError::EmptyStates;
692        assert!(err.to_string().contains("States cannot be empty"));
693
694        let err = PlaybookError::EmptyTransitions;
695        assert!(err.to_string().contains("Transitions cannot be empty"));
696    }
697
698    #[test]
699    fn test_invariant_severity_default() {
700        let severity = InvariantSeverity::default();
701        assert_eq!(severity, InvariantSeverity::Error);
702    }
703
704    #[test]
705    fn test_invariant_severity_parsing() {
706        let yaml = r#"
707version: "1.0"
708machine:
709  id: "test"
710  initial: "start"
711  states:
712    start:
713      id: "start"
714      invariants:
715        - description: "warning"
716          condition: "true"
717          severity: warning
718        - description: "error"
719          condition: "true"
720          severity: error
721        - description: "critical"
722          condition: "true"
723          severity: critical
724    end:
725      id: "end"
726  transitions:
727    - id: "t1"
728      from: "start"
729      to: "end"
730      event: "go"
731"#;
732        let playbook = Playbook::from_yaml(yaml).expect("Should parse");
733        let state = playbook.machine.states.get("start").unwrap();
734        assert_eq!(state.invariants.len(), 3);
735        assert_eq!(state.invariants[0].severity, InvariantSeverity::Warning);
736        assert_eq!(state.invariants[1].severity, InvariantSeverity::Error);
737        assert_eq!(state.invariants[2].severity, InvariantSeverity::Critical);
738    }
739
740    #[test]
741    fn test_complexity_class_parsing() {
742        let yaml = r#"
743version: "1.0"
744machine:
745  id: "test"
746  initial: "start"
747  states:
748    start:
749      id: "start"
750    end:
751      id: "end"
752  transitions:
753    - id: "t1"
754      from: "start"
755      to: "end"
756      event: "go"
757  performance:
758    complexity_class: "O(1)"
759"#;
760        let playbook = Playbook::from_yaml(yaml).expect("Should parse O(1)");
761        assert_eq!(
762            playbook.machine.performance.unwrap().complexity_class,
763            Some(ComplexityClass::Constant)
764        );
765
766        // Test all complexity classes
767        for (class_str, expected) in [
768            ("O(log n)", ComplexityClass::Logarithmic),
769            ("O(n)", ComplexityClass::Linear),
770            ("O(n log n)", ComplexityClass::Linearithmic),
771            ("O(n^2)", ComplexityClass::Quadratic),
772        ] {
773            let yaml = format!(
774                r#"
775version: "1.0"
776machine:
777  id: "test"
778  initial: "start"
779  states:
780    start:
781      id: "start"
782    end:
783      id: "end"
784  transitions:
785    - id: "t1"
786      from: "start"
787      to: "end"
788      event: "go"
789  performance:
790    complexity_class: "{class_str}"
791"#
792            );
793            let playbook = Playbook::from_yaml(&yaml).expect("Should parse");
794            assert_eq!(
795                playbook.machine.performance.unwrap().complexity_class,
796                Some(expected)
797            );
798        }
799    }
800
801    #[test]
802    fn test_action_variants_parsing() {
803        let yaml = r#"
804version: "1.0"
805machine:
806  id: "test"
807  initial: "start"
808  states:
809    start:
810      id: "start"
811      on_entry:
812        - type: click
813          selector: ".btn"
814        - type: type
815          selector: ".input"
816          text: "hello"
817        - type: wait
818          condition:
819            type: visible
820            selector: ".element"
821        - type: navigate
822          url: "https://example.com"
823        - type: script
824          code: "console.log('hi')"
825        - type: screenshot
826          name: "screenshot1"
827      on_exit:
828        - type: click
829          selector: ".logout"
830    end:
831      id: "end"
832  transitions:
833    - id: "t1"
834      from: "start"
835      to: "end"
836      event: "go"
837      actions:
838        - type: wait
839          condition:
840            type: hidden
841            selector: ".loader"
842"#;
843        let playbook = Playbook::from_yaml(yaml).expect("Should parse actions");
844        let state = playbook.machine.states.get("start").unwrap();
845
846        assert_eq!(state.on_entry.len(), 6);
847        assert!(matches!(&state.on_entry[0], Action::Click { selector } if selector == ".btn"));
848        assert!(
849            matches!(&state.on_entry[1], Action::Type { selector, text } if selector == ".input" && text == "hello")
850        );
851        assert!(matches!(&state.on_entry[2], Action::Wait { .. }));
852        assert!(
853            matches!(&state.on_entry[3], Action::Navigate { url } if url == "https://example.com")
854        );
855        assert!(
856            matches!(&state.on_entry[4], Action::Script { code } if code == "console.log('hi')")
857        );
858        assert!(matches!(&state.on_entry[5], Action::Screenshot { name } if name == "screenshot1"));
859
860        assert_eq!(state.on_exit.len(), 1);
861    }
862
863    #[test]
864    fn test_wait_condition_variants() {
865        let yaml = r#"
866version: "1.0"
867machine:
868  id: "test"
869  initial: "start"
870  states:
871    start:
872      id: "start"
873      on_entry:
874        - type: wait
875          condition:
876            type: duration
877            ms: 1000
878        - type: wait
879          condition:
880            type: network_idle
881        - type: wait
882          condition:
883            type: condition
884            expression: "window.ready === true"
885    end:
886      id: "end"
887  transitions:
888    - id: "t1"
889      from: "start"
890      to: "end"
891      event: "go"
892"#;
893        let playbook = Playbook::from_yaml(yaml).expect("Should parse wait conditions");
894        let state = playbook.machine.states.get("start").unwrap();
895
896        assert_eq!(state.on_entry.len(), 3);
897        if let Action::Wait { condition } = &state.on_entry[0] {
898            assert!(matches!(condition, WaitCondition::Duration { ms: 1000 }));
899        } else {
900            panic!("Expected Wait action");
901        }
902        if let Action::Wait { condition } = &state.on_entry[1] {
903            assert!(matches!(condition, WaitCondition::NetworkIdle));
904        } else {
905            panic!("Expected Wait action");
906        }
907        if let Action::Wait { condition } = &state.on_entry[2] {
908            assert!(matches!(condition, WaitCondition::Condition { .. }));
909        } else {
910            panic!("Expected Wait action");
911        }
912    }
913
914    #[test]
915    fn test_assertion_variants() {
916        let yaml = r#"
917version: "1.0"
918machine:
919  id: "test"
920  initial: "start"
921  states:
922    start:
923      id: "start"
924    end:
925      id: "end"
926  transitions:
927    - id: "t1"
928      from: "start"
929      to: "end"
930      event: "go"
931      assertions:
932        - type: element_exists
933          selector: ".elem"
934        - type: text_equals
935          selector: ".text"
936          expected: "Hello"
937        - type: text_contains
938          selector: ".text"
939          substring: "ell"
940        - type: attribute_equals
941          selector: ".elem"
942          attribute: "data-value"
943          expected: "123"
944        - type: url_matches
945          pattern: "^https://.*"
946        - type: script
947          expression: "document.title === 'Test'"
948"#;
949        let playbook = Playbook::from_yaml(yaml).expect("Should parse assertions");
950        let transition = &playbook.machine.transitions[0];
951
952        assert_eq!(transition.assertions.len(), 6);
953        assert!(matches!(
954            &transition.assertions[0],
955            Assertion::ElementExists { .. }
956        ));
957        assert!(matches!(
958            &transition.assertions[1],
959            Assertion::TextEquals { .. }
960        ));
961        assert!(matches!(
962            &transition.assertions[2],
963            Assertion::TextContains { .. }
964        ));
965        assert!(matches!(
966            &transition.assertions[3],
967            Assertion::AttributeEquals { .. }
968        ));
969        assert!(matches!(
970            &transition.assertions[4],
971            Assertion::UrlMatches { .. }
972        ));
973        assert!(matches!(
974            &transition.assertions[5],
975            Assertion::Script { .. }
976        ));
977    }
978
979    #[test]
980    fn test_performance_budget_defaults() {
981        let budget = PerformanceBudget::default();
982        assert!(budget.max_transition_time_ms.is_none());
983        assert!(budget.max_total_time_ms.is_none());
984        assert!(budget.max_memory_bytes.is_none());
985        assert!(budget.complexity_class.is_none());
986    }
987
988    #[test]
989    fn test_performance_budget_full() {
990        let yaml = r#"
991version: "1.0"
992machine:
993  id: "test"
994  initial: "start"
995  states:
996    start:
997      id: "start"
998    end:
999      id: "end"
1000  transitions:
1001    - id: "t1"
1002      from: "start"
1003      to: "end"
1004      event: "go"
1005performance:
1006  max_transition_time_ms: 100
1007  max_total_time_ms: 5000
1008  max_memory_bytes: 10485760
1009  complexity_class: "O(n)"
1010"#;
1011        let playbook = Playbook::from_yaml(yaml).expect("Should parse performance");
1012        assert_eq!(playbook.performance.max_transition_time_ms, Some(100));
1013        assert_eq!(playbook.performance.max_total_time_ms, Some(5000));
1014        assert_eq!(playbook.performance.max_memory_bytes, Some(10485760));
1015        assert_eq!(
1016            playbook.performance.complexity_class,
1017            Some(ComplexityClass::Linear)
1018        );
1019    }
1020
1021    #[test]
1022    fn test_forbidden_transitions() {
1023        let yaml = r#"
1024version: "1.0"
1025machine:
1026  id: "test"
1027  initial: "start"
1028  states:
1029    start:
1030      id: "start"
1031    middle:
1032      id: "middle"
1033    end:
1034      id: "end"
1035  transitions:
1036    - id: "t1"
1037      from: "start"
1038      to: "middle"
1039      event: "go"
1040    - id: "t2"
1041      from: "middle"
1042      to: "end"
1043      event: "finish"
1044  forbidden:
1045    - from: "start"
1046      to: "end"
1047      reason: "Must go through middle state"
1048    - from: "end"
1049      to: "start"
1050"#;
1051        let playbook = Playbook::from_yaml(yaml).expect("Should parse forbidden transitions");
1052        assert_eq!(playbook.machine.forbidden.len(), 2);
1053        assert_eq!(playbook.machine.forbidden[0].from, "start");
1054        assert_eq!(playbook.machine.forbidden[0].to, "end");
1055        assert_eq!(
1056            playbook.machine.forbidden[0].reason,
1057            "Must go through middle state"
1058        );
1059        assert_eq!(playbook.machine.forbidden[1].from, "end");
1060        assert_eq!(playbook.machine.forbidden[1].to, "start");
1061        assert_eq!(playbook.machine.forbidden[1].reason, "");
1062    }
1063
1064    #[test]
1065    fn test_playbook_steps() {
1066        let yaml = r#"
1067version: "1.0"
1068machine:
1069  id: "test"
1070  initial: "start"
1071  states:
1072    start:
1073      id: "start"
1074    end:
1075      id: "end"
1076  transitions:
1077    - id: "t1"
1078      from: "start"
1079      to: "end"
1080      event: "go"
1081playbook:
1082  setup:
1083    - action:
1084        wasm: "init_game"
1085        args: ["--level", "1"]
1086      description: "Initialize game"
1087      ignore_errors: false
1088  steps:
1089    - name: "Step 1"
1090      transitions: ["t1"]
1091      timeout: "30s"
1092      capture:
1093        - var: "score"
1094          from: "game.score"
1095  teardown:
1096    - action:
1097        wasm: "cleanup"
1098        args: []
1099      description: "Cleanup"
1100      ignore_errors: true
1101"#;
1102        let playbook = Playbook::from_yaml(yaml).expect("Should parse playbook steps");
1103        let steps = playbook.playbook.unwrap();
1104
1105        assert_eq!(steps.setup.len(), 1);
1106        assert_eq!(steps.setup[0].action.wasm, Some("init_game".to_string()));
1107        assert_eq!(steps.setup[0].action.args, vec!["--level", "1"]);
1108        assert_eq!(steps.setup[0].description, "Initialize game");
1109        assert!(!steps.setup[0].ignore_errors);
1110
1111        assert_eq!(steps.steps.len(), 1);
1112        assert_eq!(steps.steps[0].name, "Step 1");
1113        assert_eq!(steps.steps[0].transitions, vec!["t1"]);
1114        assert_eq!(steps.steps[0].timeout, Some("30s".to_string()));
1115        assert_eq!(steps.steps[0].capture.len(), 1);
1116        assert_eq!(steps.steps[0].capture[0].var, "score");
1117        assert_eq!(steps.steps[0].capture[0].from, "game.score");
1118
1119        assert_eq!(steps.teardown.len(), 1);
1120        assert!(steps.teardown[0].ignore_errors);
1121    }
1122
1123    #[test]
1124    fn test_playbook_assertions() {
1125        let yaml = r#"
1126version: "1.0"
1127machine:
1128  id: "test"
1129  initial: "start"
1130  states:
1131    start:
1132      id: "start"
1133    middle:
1134      id: "middle"
1135    end:
1136      id: "end"
1137  transitions:
1138    - id: "t1"
1139      from: "start"
1140      to: "middle"
1141      event: "go"
1142    - id: "t2"
1143      from: "middle"
1144      to: "end"
1145      event: "finish"
1146assertions:
1147  path:
1148    expected: ["start", "middle", "end"]
1149  output:
1150    - var: "score"
1151      not_empty: true
1152    - var: "score"
1153      matches: "^\\d+$"
1154    - var: "score"
1155      less_than: 1000
1156    - var: "score"
1157      greater_than: 0
1158    - var: "name"
1159      equals: "Player1"
1160  complexity:
1161    operation: "render"
1162    measure: "render_time"
1163    input_var: "entity_count"
1164    expected: "O(n)"
1165    tolerance: 0.1
1166    sample_sizes: [10, 100, 1000]
1167"#;
1168        let playbook = Playbook::from_yaml(yaml).expect("Should parse assertions");
1169        let assertions = playbook.assertions.unwrap();
1170
1171        assert_eq!(
1172            assertions.path.unwrap().expected,
1173            vec!["start", "middle", "end"]
1174        );
1175
1176        assert_eq!(assertions.output.len(), 5);
1177        assert_eq!(assertions.output[0].var, "score");
1178        assert_eq!(assertions.output[0].not_empty, Some(true));
1179        assert_eq!(assertions.output[1].matches, Some("^\\d+$".to_string()));
1180        assert_eq!(assertions.output[2].less_than, Some(1000));
1181        assert_eq!(assertions.output[3].greater_than, Some(0));
1182        assert_eq!(assertions.output[4].equals, Some("Player1".to_string()));
1183
1184        let complexity = assertions.complexity.unwrap();
1185        assert_eq!(complexity.operation, "render");
1186        assert_eq!(complexity.measure, "render_time");
1187        assert_eq!(complexity.input_var, "entity_count");
1188        assert_eq!(complexity.expected, ComplexityClass::Linear);
1189        assert!((complexity.tolerance - 0.1).abs() < f64::EPSILON);
1190        assert_eq!(complexity.sample_sizes, vec![10, 100, 1000]);
1191    }
1192
1193    #[test]
1194    fn test_falsification_config() {
1195        let yaml = r#"
1196version: "1.0"
1197machine:
1198  id: "test"
1199  initial: "start"
1200  states:
1201    start:
1202      id: "start"
1203    end:
1204      id: "end"
1205  transitions:
1206    - id: "t1"
1207      from: "start"
1208      to: "end"
1209      event: "go"
1210falsification:
1211  mutations:
1212    - id: "mut1"
1213      description: "Remove collision detection"
1214      mutate: "game.collision_enabled = false"
1215      expected_failure: "Player should collide with wall"
1216    - id: "mut2"
1217      mutate: "game.score = -1"
1218      expected_failure: "Score should never be negative"
1219"#;
1220        let playbook = Playbook::from_yaml(yaml).expect("Should parse falsification");
1221        let falsification = playbook.falsification.unwrap();
1222
1223        assert_eq!(falsification.mutations.len(), 2);
1224        assert_eq!(falsification.mutations[0].id, "mut1");
1225        assert_eq!(
1226            falsification.mutations[0].description,
1227            "Remove collision detection"
1228        );
1229        assert_eq!(
1230            falsification.mutations[0].mutate,
1231            "game.collision_enabled = false"
1232        );
1233        assert_eq!(
1234            falsification.mutations[0].expected_failure,
1235            "Player should collide with wall"
1236        );
1237        assert_eq!(falsification.mutations[1].id, "mut2");
1238        assert_eq!(falsification.mutations[1].description, "");
1239    }
1240
1241    #[test]
1242    fn test_metadata() {
1243        let yaml = r#"
1244version: "1.0"
1245machine:
1246  id: "test"
1247  initial: "start"
1248  states:
1249    start:
1250      id: "start"
1251    end:
1252      id: "end"
1253  transitions:
1254    - id: "t1"
1255      from: "start"
1256      to: "end"
1257      event: "go"
1258metadata:
1259  author: "Test Author"
1260  game: "MyGame"
1261  tags: "integration,smoke"
1262"#;
1263        let playbook = Playbook::from_yaml(yaml).expect("Should parse metadata");
1264        assert_eq!(playbook.metadata.get("author"), Some(&"Test Author".into()));
1265        assert_eq!(playbook.metadata.get("game"), Some(&"MyGame".into()));
1266        assert_eq!(
1267            playbook.metadata.get("tags"),
1268            Some(&"integration,smoke".into())
1269        );
1270    }
1271
1272    #[test]
1273    fn test_optional_fields_defaults() {
1274        let yaml = r#"
1275version: "1.0"
1276machine:
1277  id: "minimal"
1278  initial: "start"
1279  states:
1280    start:
1281      id: "start"
1282    end:
1283      id: "end"
1284  transitions:
1285    - id: "t1"
1286      from: "start"
1287      to: "end"
1288      event: "go"
1289"#;
1290        let playbook = Playbook::from_yaml(yaml).expect("Should parse minimal playbook");
1291        assert_eq!(playbook.name, "");
1292        assert_eq!(playbook.description, "");
1293        assert!(playbook.playbook.is_none());
1294        assert!(playbook.assertions.is_none());
1295        assert!(playbook.falsification.is_none());
1296        assert!(playbook.metadata.is_empty());
1297        assert!(playbook.machine.forbidden.is_empty());
1298        assert!(playbook.machine.performance.is_none());
1299    }
1300
1301    #[test]
1302    fn test_state_optional_fields() {
1303        let yaml = r#"
1304version: "1.0"
1305machine:
1306  id: "test"
1307  initial: "start"
1308  states:
1309    start:
1310      id: "start"
1311    end:
1312      id: "end"
1313  transitions:
1314    - id: "t1"
1315      from: "start"
1316      to: "end"
1317      event: "go"
1318"#;
1319        let playbook = Playbook::from_yaml(yaml).expect("Should parse");
1320        let state = playbook.machine.states.get("start").unwrap();
1321        assert_eq!(state.description, "");
1322        assert!(state.on_entry.is_empty());
1323        assert!(state.on_exit.is_empty());
1324        assert!(state.invariants.is_empty());
1325        assert!(!state.final_state);
1326    }
1327
1328    #[test]
1329    fn test_transition_optional_fields() {
1330        let yaml = r#"
1331version: "1.0"
1332machine:
1333  id: "test"
1334  initial: "start"
1335  states:
1336    start:
1337      id: "start"
1338    end:
1339      id: "end"
1340  transitions:
1341    - id: "t1"
1342      from: "start"
1343      to: "end"
1344      event: "go"
1345"#;
1346        let playbook = Playbook::from_yaml(yaml).expect("Should parse");
1347        let transition = &playbook.machine.transitions[0];
1348        assert!(transition.guard.is_none());
1349        assert!(transition.actions.is_empty());
1350        assert!(transition.assertions.is_empty());
1351    }
1352
1353    #[test]
1354    fn test_transition_with_guard() {
1355        let yaml = r#"
1356version: "1.0"
1357machine:
1358  id: "test"
1359  initial: "start"
1360  states:
1361    start:
1362      id: "start"
1363    end:
1364      id: "end"
1365  transitions:
1366    - id: "t1"
1367      from: "start"
1368      to: "end"
1369      event: "go"
1370      guard: "player.health > 0"
1371"#;
1372        let playbook = Playbook::from_yaml(yaml).expect("Should parse");
1373        let transition = &playbook.machine.transitions[0];
1374        assert_eq!(transition.guard, Some("player.health > 0".to_string()));
1375    }
1376
1377    #[test]
1378    fn test_playbook_steps_default() {
1379        let steps = PlaybookSteps::default();
1380        assert!(steps.setup.is_empty());
1381        assert!(steps.steps.is_empty());
1382        assert!(steps.teardown.is_empty());
1383    }
1384
1385    #[test]
1386    fn test_playbook_assertions_default() {
1387        let assertions = PlaybookAssertions::default();
1388        assert!(assertions.path.is_none());
1389        assert!(assertions.output.is_empty());
1390        assert!(assertions.complexity.is_none());
1391    }
1392
1393    #[test]
1394    fn test_falsification_config_default() {
1395        let config = FalsificationConfig::default();
1396        assert!(config.mutations.is_empty());
1397    }
1398
1399    #[test]
1400    fn test_name_and_description() {
1401        let yaml = r#"
1402version: "1.0"
1403name: "My Test Playbook"
1404description: "A comprehensive test"
1405machine:
1406  id: "test"
1407  initial: "start"
1408  states:
1409    start:
1410      id: "start"
1411    end:
1412      id: "end"
1413  transitions:
1414    - id: "t1"
1415      from: "start"
1416      to: "end"
1417      event: "go"
1418"#;
1419        let playbook = Playbook::from_yaml(yaml).expect("Should parse");
1420        assert_eq!(playbook.name, "My Test Playbook");
1421        assert_eq!(playbook.description, "A comprehensive test");
1422    }
1423
1424    #[test]
1425    fn test_playbook_error_clone() {
1426        let err = PlaybookError::InvalidVersion("2.0".to_string());
1427        let cloned = err.clone();
1428        assert_eq!(err.to_string(), cloned.to_string());
1429    }
1430
1431    #[test]
1432    fn test_playbook_error_debug() {
1433        let err = PlaybookError::EmptyStates;
1434        let debug_str = format!("{:?}", err);
1435        assert!(debug_str.contains("EmptyStates"));
1436    }
1437
1438    #[test]
1439    fn test_struct_clone_derive() {
1440        // Test Clone implementations
1441        let state = State {
1442            id: "test".to_string(),
1443            description: "desc".to_string(),
1444            on_entry: vec![],
1445            on_exit: vec![],
1446            invariants: vec![],
1447            final_state: false,
1448        };
1449        let _ = state;
1450
1451        let transition = Transition {
1452            id: "t1".to_string(),
1453            from: "a".to_string(),
1454            to: "b".to_string(),
1455            event: "go".to_string(),
1456            guard: None,
1457            actions: vec![],
1458            assertions: vec![],
1459        };
1460        let _ = transition;
1461
1462        let invariant = Invariant {
1463            description: "test".to_string(),
1464            condition: "true".to_string(),
1465            severity: InvariantSeverity::Error,
1466        };
1467        let _ = invariant;
1468
1469        let forbidden = ForbiddenTransition {
1470            from: "a".to_string(),
1471            to: "b".to_string(),
1472            reason: "test".to_string(),
1473        };
1474        let _ = forbidden;
1475    }
1476
1477    #[test]
1478    fn test_action_clone() {
1479        let action = Action::Click {
1480            selector: ".btn".to_string(),
1481        };
1482        let _ = action;
1483
1484        let action = Action::Type {
1485            selector: ".input".to_string(),
1486            text: "hello".to_string(),
1487        };
1488        let _ = action;
1489
1490        let action = Action::Wait {
1491            condition: WaitCondition::NetworkIdle,
1492        };
1493        let _ = action;
1494    }
1495
1496    #[test]
1497    fn test_wait_condition_clone() {
1498        let cond = WaitCondition::Visible {
1499            selector: ".elem".to_string(),
1500        };
1501        let _ = cond;
1502
1503        let cond = WaitCondition::Hidden {
1504            selector: ".elem".to_string(),
1505        };
1506        let _ = cond;
1507
1508        let cond = WaitCondition::Duration { ms: 100 };
1509        let _ = cond;
1510
1511        let cond = WaitCondition::Condition {
1512            expression: "true".to_string(),
1513        };
1514        let _ = cond;
1515    }
1516
1517    #[test]
1518    fn test_assertion_clone() {
1519        let assertion = Assertion::ElementExists {
1520            selector: ".elem".to_string(),
1521        };
1522        let _ = assertion;
1523
1524        let assertion = Assertion::TextEquals {
1525            selector: ".elem".to_string(),
1526            expected: "text".to_string(),
1527        };
1528        let _ = assertion;
1529
1530        let assertion = Assertion::TextContains {
1531            selector: ".elem".to_string(),
1532            substring: "text".to_string(),
1533        };
1534        let _ = assertion;
1535
1536        let assertion = Assertion::AttributeEquals {
1537            selector: ".elem".to_string(),
1538            attribute: "attr".to_string(),
1539            expected: "val".to_string(),
1540        };
1541        let _ = assertion;
1542
1543        let assertion = Assertion::UrlMatches {
1544            pattern: ".*".to_string(),
1545        };
1546        let _ = assertion;
1547
1548        let assertion = Assertion::Script {
1549            expression: "true".to_string(),
1550        };
1551        let _ = assertion;
1552    }
1553
1554    #[test]
1555    fn test_complexity_class_copy() {
1556        let class = ComplexityClass::Constant;
1557        let copied = class;
1558        assert_eq!(class, copied);
1559    }
1560
1561    #[test]
1562    fn test_invariant_severity_copy() {
1563        let severity = InvariantSeverity::Warning;
1564        let copied = severity;
1565        assert_eq!(severity, copied);
1566    }
1567
1568    #[test]
1569    fn test_action_spec_clone() {
1570        let spec = ActionSpec {
1571            wasm: Some("func".to_string()),
1572            args: vec!["arg1".to_string()],
1573        };
1574        let _ = spec;
1575    }
1576
1577    #[test]
1578    fn test_playbook_action_clone() {
1579        let action = PlaybookAction {
1580            action: ActionSpec {
1581                wasm: None,
1582                args: vec![],
1583            },
1584            description: "test".to_string(),
1585            ignore_errors: true,
1586        };
1587        let _ = action;
1588    }
1589
1590    #[test]
1591    fn test_playbook_step_clone() {
1592        let step = PlaybookStep {
1593            name: "step1".to_string(),
1594            transitions: vec!["t1".to_string()],
1595            timeout: Some("10s".to_string()),
1596            capture: vec![VariableCapture {
1597                var: "x".to_string(),
1598                from: "y".to_string(),
1599            }],
1600        };
1601        let _ = step;
1602    }
1603
1604    #[test]
1605    fn test_variable_capture_clone() {
1606        let capture = VariableCapture {
1607            var: "x".to_string(),
1608            from: "y".to_string(),
1609        };
1610        let _ = capture;
1611    }
1612
1613    #[test]
1614    fn test_path_assertion_clone() {
1615        let assertion = PathAssertion {
1616            expected: vec!["a".to_string(), "b".to_string()],
1617        };
1618        let _ = assertion;
1619    }
1620
1621    #[test]
1622    fn test_output_assertion_clone() {
1623        let assertion = OutputAssertion {
1624            var: "x".to_string(),
1625            not_empty: Some(true),
1626            matches: Some(".*".to_string()),
1627            less_than: Some(100),
1628            greater_than: Some(0),
1629            equals: Some("value".to_string()),
1630        };
1631        let _ = assertion;
1632    }
1633
1634    #[test]
1635    fn test_complexity_assertion_clone() {
1636        let assertion = ComplexityAssertion {
1637            operation: "op".to_string(),
1638            measure: "time".to_string(),
1639            input_var: "n".to_string(),
1640            expected: ComplexityClass::Linear,
1641            tolerance: 0.1,
1642            sample_sizes: vec![10, 100],
1643        };
1644        let _ = assertion;
1645    }
1646
1647    #[test]
1648    fn test_mutation_def_clone() {
1649        let mutation = MutationDef {
1650            id: "m1".to_string(),
1651            description: "desc".to_string(),
1652            mutate: "x = 1".to_string(),
1653            expected_failure: "fail".to_string(),
1654        };
1655        let _ = mutation;
1656    }
1657
1658    #[test]
1659    fn test_struct_debug_derive() {
1660        // Test Debug implementations
1661        let state = State {
1662            id: "test".to_string(),
1663            description: "desc".to_string(),
1664            on_entry: vec![],
1665            on_exit: vec![],
1666            invariants: vec![],
1667            final_state: false,
1668        };
1669        let _ = format!("{:?}", state);
1670
1671        let action = Action::Click {
1672            selector: ".btn".to_string(),
1673        };
1674        let _ = format!("{:?}", action);
1675
1676        let cond = WaitCondition::NetworkIdle;
1677        let _ = format!("{:?}", cond);
1678
1679        let assertion = Assertion::Script {
1680            expression: "true".to_string(),
1681        };
1682        let _ = format!("{:?}", assertion);
1683
1684        let severity = InvariantSeverity::Critical;
1685        let _ = format!("{:?}", severity);
1686
1687        let class = ComplexityClass::Quadratic;
1688        let _ = format!("{:?}", class);
1689    }
1690
1691    #[test]
1692    fn test_playbook_clone() {
1693        let playbook = Playbook::from_yaml(VALID_PLAYBOOK).expect("Should parse");
1694        let _ = playbook;
1695    }
1696
1697    #[test]
1698    fn test_state_machine_clone() {
1699        let playbook = Playbook::from_yaml(VALID_PLAYBOOK).expect("Should parse");
1700        let _ = playbook.machine;
1701    }
1702}