runtara_dsl/
lib.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! DSL Type Definitions - Single Source of Truth
4//!
5//! This crate defines the scenario DSL types used throughout the codebase:
6//! - Runtime deserialization of scenario JSON
7//! - Compiler type-safe access to scenario structure
8//! - Auto-generation of JSON Schema via schemars (in build.rs)
9//!
10//! Changes to these types automatically update `specs/dsl/v{VERSION}/schema.json` on rebuild.
11
12// Provide imports needed by schema_types.rs
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17// Include the schema types
18include!("schema_types.rs");
19
20// Path utilities
21pub mod paths;
22
23// Agent capability metadata types for runtime introspection
24pub mod agent_meta;
25
26// Specification generation (DSL schema, OpenAPI, compatibility)
27pub mod spec;
28
29// Step type metadata registration (auto-registers step types with inventory)
30mod step_registration;
31
32// ============================================================================
33// Parsing Functions
34// ============================================================================
35
36/// Parse an execution graph from JSON Value
37pub fn parse_execution_graph(json: &serde_json::Value) -> Result<ExecutionGraph, String> {
38    serde_json::from_value(json.clone())
39        .map_err(|e| format!("Failed to parse execution graph: {}", e))
40}
41
42/// Parse a complete scenario from JSON Value
43pub fn parse_scenario(json: &serde_json::Value) -> Result<Scenario, String> {
44    serde_json::from_value(json.clone()).map_err(|e| format!("Failed to parse scenario: {}", e))
45}
46
47/// Metadata about a step type for documentation
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct StepTypeInfo {
50    #[serde(rename = "type")]
51    pub step_type: String,
52    pub category: String,
53    pub description: String,
54}
55
56/// Get metadata for all step types (collected via inventory from step_registration.rs)
57///
58/// This function returns step type metadata that is automatically derived from
59/// the actual step struct definitions, ensuring the DSL schema is always in sync
60/// with the implementation.
61pub fn get_step_types() -> Vec<StepTypeInfo> {
62    // Start step is a virtual step (not a struct), add it manually
63    let mut steps = vec![StepTypeInfo {
64        step_type: "Start".to_string(),
65        category: "control".to_string(),
66        description: "Entry point - receives scenario inputs".to_string(),
67    }];
68
69    // Collect step types registered via inventory
70    for meta in agent_meta::get_all_step_types() {
71        steps.push(StepTypeInfo {
72            step_type: meta.id.to_string(),
73            category: meta.category.to_string(),
74            description: meta.description.to_string(),
75        });
76    }
77
78    // Sort by step type for consistent ordering
79    steps.sort_by(|a, b| a.step_type.cmp(&b.step_type));
80
81    steps
82}
83
84// ============================================================================
85// MemoryTier Methods
86// ============================================================================
87
88impl MemoryTier {
89    /// Total memory allocation in bytes
90    pub fn total_memory_bytes(&self) -> usize {
91        match self {
92            MemoryTier::S => 8 * 1024 * 1024,    // 8MB
93            MemoryTier::M => 64 * 1024 * 1024,   // 64MB
94            MemoryTier::L => 128 * 1024 * 1024,  // 128MB
95            MemoryTier::XL => 256 * 1024 * 1024, // 256MB
96        }
97    }
98
99    /// Stack size in bytes
100    pub fn stack_size_bytes(&self) -> usize {
101        match self {
102            MemoryTier::S => 1 * 1024 * 1024,  // 1MB
103            MemoryTier::M => 4 * 1024 * 1024,  // 4MB
104            MemoryTier::L => 8 * 1024 * 1024,  // 8MB
105            MemoryTier::XL => 8 * 1024 * 1024, // 8MB
106        }
107    }
108
109    /// Get as string
110    pub fn as_str(&self) -> &'static str {
111        match self {
112            MemoryTier::S => "S",
113            MemoryTier::M => "M",
114            MemoryTier::L => "L",
115            MemoryTier::XL => "XL",
116        }
117    }
118
119    /// Parse from string (case-insensitive)
120    pub fn from_str(s: &str) -> Option<Self> {
121        match s.to_uppercase().as_str() {
122            "S" => Some(MemoryTier::S),
123            "M" => Some(MemoryTier::M),
124            "L" => Some(MemoryTier::L),
125            "XL" => Some(MemoryTier::XL),
126            _ => None,
127        }
128    }
129}
130
131impl std::fmt::Display for MemoryTier {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "{}", self.as_str())
134    }
135}
136
137// ============================================================================
138// SchemaFieldType Helper Methods
139// ============================================================================
140
141impl SchemaFieldType {
142    /// Get as string representation
143    pub fn as_str(&self) -> &'static str {
144        match self {
145            SchemaFieldType::String => "string",
146            SchemaFieldType::Integer => "integer",
147            SchemaFieldType::Number => "number",
148            SchemaFieldType::Boolean => "boolean",
149            SchemaFieldType::Array => "array",
150            SchemaFieldType::Object => "object",
151            SchemaFieldType::File => "file",
152        }
153    }
154}
155
156impl std::fmt::Display for SchemaFieldType {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        write!(f, "{}", self.as_str())
159    }
160}
161
162impl From<&SchemaFieldType> for String {
163    fn from(t: &SchemaFieldType) -> Self {
164        t.as_str().to_string()
165    }
166}
167
168// ============================================================================
169// MappingValue Helper Methods
170// ============================================================================
171
172impl MappingValue {
173    /// Check if this is a reference (dynamic data lookup)
174    pub fn is_reference(&self) -> bool {
175        matches!(self, MappingValue::Reference(_))
176    }
177
178    /// Check if this is an immediate (static/literal) value
179    pub fn is_immediate(&self) -> bool {
180        matches!(self, MappingValue::Immediate(_))
181    }
182
183    /// Check if this is a composite (structured object/array with nested MappingValues)
184    pub fn is_composite(&self) -> bool {
185        matches!(self, MappingValue::Composite(_))
186    }
187
188    /// Get the string value if this is a reference
189    pub fn as_reference_str(&self) -> Option<&str> {
190        match self {
191            MappingValue::Reference(r) => Some(&r.value),
192            _ => None,
193        }
194    }
195
196    /// Get the value if this is an immediate
197    pub fn as_immediate_value(&self) -> Option<&serde_json::Value> {
198        match self {
199            MappingValue::Immediate(i) => Some(&i.value),
200            _ => None,
201        }
202    }
203
204    /// Get the inner composite value if this is a composite
205    pub fn as_composite(&self) -> Option<&CompositeInner> {
206        match self {
207            MappingValue::Composite(c) => Some(&c.value),
208            _ => None,
209        }
210    }
211
212    /// Recursively collect all reference paths used in this MappingValue
213    pub fn collect_references(&self) -> Vec<&str> {
214        match self {
215            MappingValue::Reference(r) => vec![r.value.as_str()],
216            MappingValue::Immediate(_) => vec![],
217            MappingValue::Composite(c) => c.value.collect_references(),
218        }
219    }
220
221    /// Returns true if this value or any nested value contains references
222    pub fn has_references(&self) -> bool {
223        match self {
224            MappingValue::Reference(_) => true,
225            MappingValue::Immediate(_) => false,
226            MappingValue::Composite(c) => c.value.has_references(),
227        }
228    }
229}
230
231// ============================================================================
232// CompositeInner Helper Methods
233// ============================================================================
234
235impl CompositeInner {
236    /// Check if this is an object composite
237    pub fn is_object(&self) -> bool {
238        matches!(self, CompositeInner::Object(_))
239    }
240
241    /// Check if this is an array composite
242    pub fn is_array(&self) -> bool {
243        matches!(self, CompositeInner::Array(_))
244    }
245
246    /// Get the fields if this is an object composite
247    pub fn as_object(&self) -> Option<&HashMap<String, MappingValue>> {
248        match self {
249            CompositeInner::Object(map) => Some(map),
250            _ => None,
251        }
252    }
253
254    /// Get the elements if this is an array composite
255    pub fn as_array(&self) -> Option<&Vec<MappingValue>> {
256        match self {
257            CompositeInner::Array(arr) => Some(arr),
258            _ => None,
259        }
260    }
261
262    /// Recursively collect all reference paths in this composite
263    pub fn collect_references(&self) -> Vec<&str> {
264        match self {
265            CompositeInner::Object(map) => {
266                map.values().flat_map(|v| v.collect_references()).collect()
267            }
268            CompositeInner::Array(arr) => arr.iter().flat_map(|v| v.collect_references()).collect(),
269        }
270    }
271
272    /// Returns true if any nested value contains references
273    pub fn has_references(&self) -> bool {
274        match self {
275            CompositeInner::Object(map) => map.values().any(|v| v.has_references()),
276            CompositeInner::Array(arr) => arr.iter().any(|v| v.has_references()),
277        }
278    }
279}
280
281// ============================================================================
282// Tests
283// ============================================================================
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_get_step_types_from_inventory() {
291        let step_types = get_step_types();
292
293        // Should have at least 7 step types (Start + 6 registered)
294        assert!(
295            step_types.len() >= 7,
296            "Expected at least 7 step types, got {}",
297            step_types.len()
298        );
299
300        // Verify expected step types are present
301        let step_ids: Vec<&str> = step_types.iter().map(|s| s.step_type.as_str()).collect();
302
303        assert!(step_ids.contains(&"Start"), "Missing Start step type");
304        assert!(step_ids.contains(&"Finish"), "Missing Finish step type");
305        assert!(step_ids.contains(&"Agent"), "Missing Agent step type");
306        assert!(
307            step_ids.contains(&"Conditional"),
308            "Missing Conditional step type"
309        );
310        assert!(step_ids.contains(&"Split"), "Missing Split step type");
311        assert!(step_ids.contains(&"Switch"), "Missing Switch step type");
312        assert!(
313            step_ids.contains(&"StartScenario"),
314            "Missing StartScenario step type"
315        );
316    }
317
318    #[test]
319    fn test_step_type_categories() {
320        let step_types = get_step_types();
321
322        for step in &step_types {
323            match step.step_type.as_str() {
324                "Agent" | "StartScenario" => {
325                    assert_eq!(
326                        step.category, "execution",
327                        "{} should be execution category",
328                        step.step_type
329                    );
330                }
331                "Start" | "Finish" | "Conditional" | "Split" | "Switch" => {
332                    assert_eq!(
333                        step.category, "control",
334                        "{} should be control category",
335                        step.step_type
336                    );
337                }
338                _ => {}
339            }
340        }
341    }
342
343    #[test]
344    fn test_step_type_schema_generation() {
345        // Verify that schema generation functions work
346        for meta in agent_meta::get_all_step_types() {
347            let schema = (meta.schema_fn)();
348            // Just verify it doesn't panic and returns something
349            assert!(
350                schema.schema.metadata.is_some() || schema.definitions.len() > 0,
351                "Schema for {} should have metadata or definitions",
352                meta.id
353            );
354        }
355    }
356
357    // ========================================================================
358    // New Type Tests (v3.0.0)
359    // ========================================================================
360
361    #[test]
362    fn test_value_type_serialization() {
363        // Test new type names serialize correctly
364        assert_eq!(
365            serde_json::to_string(&ValueType::Integer).unwrap(),
366            "\"integer\""
367        );
368        assert_eq!(
369            serde_json::to_string(&ValueType::Number).unwrap(),
370            "\"number\""
371        );
372        assert_eq!(
373            serde_json::to_string(&ValueType::Boolean).unwrap(),
374            "\"boolean\""
375        );
376        assert_eq!(
377            serde_json::to_string(&ValueType::String).unwrap(),
378            "\"string\""
379        );
380        assert_eq!(serde_json::to_string(&ValueType::Json).unwrap(), "\"json\"");
381        assert_eq!(serde_json::to_string(&ValueType::File).unwrap(), "\"file\"");
382    }
383
384    #[test]
385    fn test_value_type_deserialization() {
386        // Test new type names deserialize correctly
387        assert_eq!(
388            serde_json::from_str::<ValueType>("\"integer\"").unwrap(),
389            ValueType::Integer
390        );
391        assert_eq!(
392            serde_json::from_str::<ValueType>("\"number\"").unwrap(),
393            ValueType::Number
394        );
395        assert_eq!(
396            serde_json::from_str::<ValueType>("\"boolean\"").unwrap(),
397            ValueType::Boolean
398        );
399    }
400
401    #[test]
402    fn test_schema_field_type_serialization() {
403        assert_eq!(
404            serde_json::to_string(&SchemaFieldType::String).unwrap(),
405            "\"string\""
406        );
407        assert_eq!(
408            serde_json::to_string(&SchemaFieldType::Integer).unwrap(),
409            "\"integer\""
410        );
411        assert_eq!(
412            serde_json::to_string(&SchemaFieldType::Number).unwrap(),
413            "\"number\""
414        );
415        assert_eq!(
416            serde_json::to_string(&SchemaFieldType::Boolean).unwrap(),
417            "\"boolean\""
418        );
419        assert_eq!(
420            serde_json::to_string(&SchemaFieldType::Array).unwrap(),
421            "\"array\""
422        );
423        assert_eq!(
424            serde_json::to_string(&SchemaFieldType::Object).unwrap(),
425            "\"object\""
426        );
427    }
428
429    #[test]
430    fn test_schema_field_type_as_str() {
431        assert_eq!(SchemaFieldType::String.as_str(), "string");
432        assert_eq!(SchemaFieldType::Integer.as_str(), "integer");
433        assert_eq!(SchemaFieldType::Number.as_str(), "number");
434        assert_eq!(SchemaFieldType::Boolean.as_str(), "boolean");
435        assert_eq!(SchemaFieldType::Array.as_str(), "array");
436        assert_eq!(SchemaFieldType::Object.as_str(), "object");
437    }
438
439    #[test]
440    fn test_schema_field_type_display() {
441        assert_eq!(format!("{}", SchemaFieldType::String), "string");
442        assert_eq!(format!("{}", SchemaFieldType::Integer), "integer");
443    }
444
445    #[test]
446    fn test_switch_match_type_serialization() {
447        assert_eq!(
448            serde_json::to_string(&SwitchMatchType::Eq).unwrap(),
449            "\"EQ\""
450        );
451        assert_eq!(
452            serde_json::to_string(&SwitchMatchType::Gt).unwrap(),
453            "\"GT\""
454        );
455        assert_eq!(
456            serde_json::to_string(&SwitchMatchType::Between).unwrap(),
457            "\"BETWEEN\""
458        );
459    }
460
461    #[test]
462    fn test_switch_config_serialization() {
463        let config = SwitchConfig {
464            value: MappingValue::Reference(ReferenceValue {
465                value: "data.status".to_string(),
466                type_hint: None,
467                default: None,
468            }),
469            cases: vec![SwitchCase {
470                match_type: SwitchMatchType::Eq,
471                match_value: serde_json::json!("active"),
472                output: serde_json::json!({"result": true}),
473            }],
474            default: Some(serde_json::json!({"result": false})),
475        };
476
477        let json = serde_json::to_value(&config).unwrap();
478        assert!(json.get("value").is_some());
479        assert!(json.get("cases").is_some());
480        assert!(json.get("default").is_some());
481    }
482
483    #[test]
484    fn test_split_config_serialization() {
485        let config = SplitConfig {
486            value: MappingValue::Reference(ReferenceValue {
487                value: "data.items".to_string(),
488                type_hint: None,
489                default: None,
490            }),
491            parallelism: Some(5),
492            sequential: Some(false),
493            dont_stop_on_failed: Some(true),
494            variables: None,
495            max_retries: None,
496            retry_delay: None,
497            timeout: None,
498        };
499
500        let json = serde_json::to_value(&config).unwrap();
501        assert!(json.get("value").is_some());
502        assert_eq!(json.get("parallelism").unwrap(), 5);
503        assert_eq!(json.get("sequential").unwrap(), false);
504        assert_eq!(json.get("dontStopOnFailed").unwrap(), true);
505    }
506
507    #[test]
508    fn test_switch_step_with_config() {
509        let step = SwitchStep {
510            id: "switch1".to_string(),
511            name: Some("My Switch".to_string()),
512            config: Some(SwitchConfig {
513                value: MappingValue::Immediate(ImmediateValue {
514                    value: serde_json::json!("test"),
515                }),
516                cases: vec![],
517                default: None,
518            }),
519        };
520
521        let json = serde_json::to_value(&step).unwrap();
522        assert_eq!(json.get("id").unwrap(), "switch1");
523        assert!(json.get("config").is_some());
524    }
525
526    #[test]
527    fn test_split_step_with_config() {
528        let step = SplitStep {
529            id: "split1".to_string(),
530            name: None,
531            subgraph: Box::new(ExecutionGraph {
532                name: None,
533                description: None,
534                steps: HashMap::new(),
535                entry_point: "start".to_string(),
536                execution_plan: vec![],
537                variables: HashMap::new(),
538                input_schema: HashMap::new(),
539                output_schema: HashMap::new(),
540                notes: None,
541                nodes: None,
542                edges: None,
543            }),
544            config: Some(SplitConfig {
545                value: MappingValue::Reference(ReferenceValue {
546                    value: "data.items".to_string(),
547                    type_hint: None,
548                    default: None,
549                }),
550                parallelism: None,
551                sequential: None,
552                dont_stop_on_failed: None,
553                variables: None,
554                max_retries: None,
555                retry_delay: None,
556                timeout: None,
557            }),
558            input_schema: HashMap::new(),
559            output_schema: HashMap::new(),
560        };
561
562        let json = serde_json::to_value(&step).unwrap();
563        assert_eq!(json.get("id").unwrap(), "split1");
564        assert!(json.get("config").is_some());
565        assert!(json.get("subgraph").is_some());
566    }
567
568    #[test]
569    fn test_dsl_version() {
570        assert_eq!(DSL_VERSION, "3.0.0");
571    }
572
573    // ========================================================================
574    // Parsing Functions Tests
575    // ========================================================================
576
577    #[test]
578    fn test_parse_execution_graph_minimal() {
579        let json = serde_json::json!({
580            "entryPoint": "start",
581            "steps": {},
582            "executionPlan": [],
583            "variables": {},
584            "inputSchema": {},
585            "outputSchema": {}
586        });
587
588        let graph = parse_execution_graph(&json).expect("Should parse minimal graph");
589        assert_eq!(graph.entry_point, "start");
590        assert!(graph.steps.is_empty());
591    }
592
593    #[test]
594    fn test_parse_execution_graph_with_steps() {
595        // Step enum uses #[serde(tag = "stepType")] - internally tagged representation
596        let json = serde_json::json!({
597            "entryPoint": "step1",
598            "steps": {
599                "step1": {
600                    "stepType": "Finish",
601                    "id": "step1",
602                    "name": "End Step"
603                }
604            },
605            "executionPlan": [
606                { "fromStep": "start", "toStep": "step1" }
607            ],
608            "variables": {},
609            "inputSchema": {},
610            "outputSchema": {}
611        });
612
613        let graph = parse_execution_graph(&json).expect("Should parse graph with steps");
614        assert_eq!(graph.entry_point, "step1");
615        assert_eq!(graph.steps.len(), 1);
616        assert!(graph.steps.contains_key("step1"));
617    }
618
619    #[test]
620    fn test_parse_execution_graph_invalid_json() {
621        let json = serde_json::json!({
622            "wrong_field": "value"
623        });
624
625        let result = parse_execution_graph(&json);
626        assert!(result.is_err());
627        assert!(result.unwrap_err().contains("Failed to parse"));
628    }
629
630    #[test]
631    fn test_parse_scenario_minimal() {
632        let json = serde_json::json!({
633            "executionGraph": {
634                "name": "Test Scenario",
635                "description": "A test",
636                "entryPoint": "start",
637                "steps": {},
638                "executionPlan": [],
639                "variables": {},
640                "inputSchema": {},
641                "outputSchema": {}
642            }
643        });
644
645        let scenario = parse_scenario(&json).expect("Should parse minimal scenario");
646        assert_eq!(
647            scenario.execution_graph.name.as_deref(),
648            Some("Test Scenario")
649        );
650        assert_eq!(
651            scenario.execution_graph.description.as_deref(),
652            Some("A test")
653        );
654    }
655
656    #[test]
657    fn test_parse_scenario_with_metadata() {
658        let json = serde_json::json!({
659            "memoryTier": "L",
660            "debugMode": true,
661            "executionGraph": {
662                "name": "Complete Scenario",
663                "description": "With metadata",
664                "entryPoint": "start",
665                "steps": {},
666                "executionPlan": [],
667                "variables": {},
668                "inputSchema": {},
669                "outputSchema": {}
670            }
671        });
672
673        let scenario = parse_scenario(&json).expect("Should parse scenario with metadata");
674        assert_eq!(
675            scenario.execution_graph.name.as_deref(),
676            Some("Complete Scenario")
677        );
678        assert_eq!(scenario.memory_tier, Some(MemoryTier::L));
679        assert_eq!(scenario.debug_mode, Some(true));
680    }
681
682    #[test]
683    fn test_parse_scenario_invalid() {
684        let json = serde_json::json!({
685            "not_a_scenario": true
686        });
687
688        let result = parse_scenario(&json);
689        assert!(result.is_err());
690        assert!(result.unwrap_err().contains("Failed to parse scenario"));
691    }
692
693    // ========================================================================
694    // MemoryTier Tests
695    // ========================================================================
696
697    #[test]
698    fn test_memory_tier_total_memory_bytes() {
699        assert_eq!(MemoryTier::S.total_memory_bytes(), 8 * 1024 * 1024);
700        assert_eq!(MemoryTier::M.total_memory_bytes(), 64 * 1024 * 1024);
701        assert_eq!(MemoryTier::L.total_memory_bytes(), 128 * 1024 * 1024);
702        assert_eq!(MemoryTier::XL.total_memory_bytes(), 256 * 1024 * 1024);
703    }
704
705    #[test]
706    fn test_memory_tier_stack_size_bytes() {
707        assert_eq!(MemoryTier::S.stack_size_bytes(), 1 * 1024 * 1024);
708        assert_eq!(MemoryTier::M.stack_size_bytes(), 4 * 1024 * 1024);
709        assert_eq!(MemoryTier::L.stack_size_bytes(), 8 * 1024 * 1024);
710        assert_eq!(MemoryTier::XL.stack_size_bytes(), 8 * 1024 * 1024);
711    }
712
713    #[test]
714    fn test_memory_tier_as_str() {
715        assert_eq!(MemoryTier::S.as_str(), "S");
716        assert_eq!(MemoryTier::M.as_str(), "M");
717        assert_eq!(MemoryTier::L.as_str(), "L");
718        assert_eq!(MemoryTier::XL.as_str(), "XL");
719    }
720
721    #[test]
722    fn test_memory_tier_from_str() {
723        assert_eq!(MemoryTier::from_str("S"), Some(MemoryTier::S));
724        assert_eq!(MemoryTier::from_str("M"), Some(MemoryTier::M));
725        assert_eq!(MemoryTier::from_str("L"), Some(MemoryTier::L));
726        assert_eq!(MemoryTier::from_str("XL"), Some(MemoryTier::XL));
727    }
728
729    #[test]
730    fn test_memory_tier_from_str_case_insensitive() {
731        assert_eq!(MemoryTier::from_str("s"), Some(MemoryTier::S));
732        assert_eq!(MemoryTier::from_str("m"), Some(MemoryTier::M));
733        assert_eq!(MemoryTier::from_str("l"), Some(MemoryTier::L));
734        assert_eq!(MemoryTier::from_str("xl"), Some(MemoryTier::XL));
735        assert_eq!(MemoryTier::from_str("Xl"), Some(MemoryTier::XL));
736        assert_eq!(MemoryTier::from_str("xL"), Some(MemoryTier::XL));
737    }
738
739    #[test]
740    fn test_memory_tier_from_str_invalid() {
741        assert_eq!(MemoryTier::from_str("XXL"), None);
742        assert_eq!(MemoryTier::from_str(""), None);
743        assert_eq!(MemoryTier::from_str("invalid"), None);
744        assert_eq!(MemoryTier::from_str("SM"), None);
745    }
746
747    #[test]
748    fn test_memory_tier_display() {
749        assert_eq!(format!("{}", MemoryTier::S), "S");
750        assert_eq!(format!("{}", MemoryTier::M), "M");
751        assert_eq!(format!("{}", MemoryTier::L), "L");
752        assert_eq!(format!("{}", MemoryTier::XL), "XL");
753    }
754
755    #[test]
756    fn test_memory_tier_serialization() {
757        assert_eq!(serde_json::to_string(&MemoryTier::S).unwrap(), "\"S\"");
758        assert_eq!(serde_json::to_string(&MemoryTier::XL).unwrap(), "\"XL\"");
759    }
760
761    #[test]
762    fn test_memory_tier_deserialization() {
763        assert_eq!(
764            serde_json::from_str::<MemoryTier>("\"S\"").unwrap(),
765            MemoryTier::S
766        );
767        assert_eq!(
768            serde_json::from_str::<MemoryTier>("\"XL\"").unwrap(),
769            MemoryTier::XL
770        );
771    }
772
773    // ========================================================================
774    // MappingValue Helper Methods Tests
775    // ========================================================================
776
777    #[test]
778    fn test_mapping_value_is_reference() {
779        let ref_val = MappingValue::Reference(ReferenceValue {
780            value: "data.field".to_string(),
781            type_hint: None,
782            default: None,
783        });
784        let imm_val = MappingValue::Immediate(ImmediateValue {
785            value: serde_json::json!("static"),
786        });
787
788        assert!(ref_val.is_reference());
789        assert!(!ref_val.is_immediate());
790        assert!(!imm_val.is_reference());
791        assert!(imm_val.is_immediate());
792    }
793
794    #[test]
795    fn test_mapping_value_as_reference_str() {
796        let ref_val = MappingValue::Reference(ReferenceValue {
797            value: "steps.agent1.outputs.data".to_string(),
798            type_hint: None,
799            default: None,
800        });
801        let imm_val = MappingValue::Immediate(ImmediateValue {
802            value: serde_json::json!("static"),
803        });
804
805        assert_eq!(
806            ref_val.as_reference_str(),
807            Some("steps.agent1.outputs.data")
808        );
809        assert_eq!(imm_val.as_reference_str(), None);
810    }
811
812    #[test]
813    fn test_mapping_value_as_immediate_value() {
814        let ref_val = MappingValue::Reference(ReferenceValue {
815            value: "data.field".to_string(),
816            type_hint: None,
817            default: None,
818        });
819        let imm_val = MappingValue::Immediate(ImmediateValue {
820            value: serde_json::json!({"key": "value"}),
821        });
822
823        assert!(ref_val.as_immediate_value().is_none());
824        assert_eq!(
825            imm_val.as_immediate_value(),
826            Some(&serde_json::json!({"key": "value"}))
827        );
828    }
829
830    #[test]
831    fn test_mapping_value_reference_with_type_hint() {
832        let ref_val = MappingValue::Reference(ReferenceValue {
833            value: "data.count".to_string(),
834            type_hint: Some(ValueType::Integer),
835            default: None,
836        });
837
838        assert!(ref_val.is_reference());
839        if let MappingValue::Reference(r) = ref_val {
840            assert_eq!(r.type_hint, Some(ValueType::Integer));
841        }
842    }
843
844    #[test]
845    fn test_mapping_value_reference_with_default() {
846        let ref_val = MappingValue::Reference(ReferenceValue {
847            value: "data.optional".to_string(),
848            type_hint: None,
849            default: Some(serde_json::json!("default_value")),
850        });
851
852        if let MappingValue::Reference(r) = ref_val {
853            assert_eq!(r.default, Some(serde_json::json!("default_value")));
854        }
855    }
856
857    // ========================================================================
858    // SchemaFieldType Tests
859    // ========================================================================
860
861    #[test]
862    fn test_schema_field_type_from_string() {
863        let s: String = (&SchemaFieldType::String).into();
864        assert_eq!(s, "string");
865
866        let i: String = (&SchemaFieldType::Integer).into();
867        assert_eq!(i, "integer");
868
869        let o: String = (&SchemaFieldType::Object).into();
870        assert_eq!(o, "object");
871    }
872
873    // ========================================================================
874    // StepTypeInfo Tests
875    // ========================================================================
876
877    #[test]
878    fn test_step_type_info_serialization() {
879        let info = StepTypeInfo {
880            step_type: "Agent".to_string(),
881            category: "execution".to_string(),
882            description: "Execute an agent capability".to_string(),
883        };
884
885        let json = serde_json::to_value(&info).unwrap();
886        assert_eq!(json.get("type").unwrap(), "Agent");
887        assert_eq!(json.get("category").unwrap(), "execution");
888    }
889
890    #[test]
891    fn test_step_type_info_deserialization() {
892        let json = serde_json::json!({
893            "type": "Conditional",
894            "category": "control",
895            "description": "Branch based on condition"
896        });
897
898        let info: StepTypeInfo = serde_json::from_value(json).unwrap();
899        assert_eq!(info.step_type, "Conditional");
900        assert_eq!(info.category, "control");
901    }
902
903    #[test]
904    fn test_get_step_types_sorted() {
905        let step_types = get_step_types();
906
907        // Verify sorted order
908        for i in 1..step_types.len() {
909            assert!(
910                step_types[i - 1].step_type <= step_types[i].step_type,
911                "Step types should be sorted alphabetically"
912            );
913        }
914    }
915
916    #[test]
917    fn test_get_step_types_includes_start() {
918        let step_types = get_step_types();
919
920        let start = step_types.iter().find(|s| s.step_type == "Start");
921        assert!(start.is_some(), "Start step should be included");
922        let start = start.unwrap();
923        assert_eq!(start.category, "control");
924        assert!(start.description.contains("Entry point"));
925    }
926
927    // ========================================================================
928    // ReferenceValue and ImmediateValue Tests
929    // ========================================================================
930
931    #[test]
932    fn test_reference_value_serialization() {
933        let ref_val = ReferenceValue {
934            value: "steps.agent.outputs.result".to_string(),
935            type_hint: Some(ValueType::String),
936            default: Some(serde_json::json!("fallback")),
937        };
938
939        let json = serde_json::to_value(&ref_val).unwrap();
940        // ReferenceValue uses "value" field (not "$ref")
941        assert_eq!(json.get("value").unwrap(), "steps.agent.outputs.result");
942        // type_hint is serialized as "type" (not "typeHint")
943        assert_eq!(json.get("type").unwrap(), "string");
944        assert_eq!(json.get("default").unwrap(), "fallback");
945    }
946
947    #[test]
948    fn test_immediate_value_serialization() {
949        let imm_val = ImmediateValue {
950            value: serde_json::json!({
951                "nested": {
952                    "array": [1, 2, 3]
953                }
954            }),
955        };
956
957        let json = serde_json::to_value(&imm_val).unwrap();
958        // ImmediateValue wraps in "value" field
959        let value = json.get("value").expect("Should have value field");
960        assert!(value.get("nested").is_some());
961    }
962
963    #[test]
964    fn test_mapping_value_round_trip() {
965        // Reference
966        let original = MappingValue::Reference(ReferenceValue {
967            value: "data.path".to_string(),
968            type_hint: None,
969            default: None,
970        });
971        let json = serde_json::to_string(&original).unwrap();
972        let parsed: MappingValue = serde_json::from_str(&json).unwrap();
973        assert!(parsed.is_reference());
974        assert_eq!(parsed.as_reference_str(), Some("data.path"));
975
976        // Immediate
977        let original_imm = MappingValue::Immediate(ImmediateValue {
978            value: serde_json::json!(42),
979        });
980        let json_imm = serde_json::to_string(&original_imm).unwrap();
981        let parsed_imm: MappingValue = serde_json::from_str(&json_imm).unwrap();
982        assert!(parsed_imm.is_immediate());
983        assert_eq!(
984            parsed_imm.as_immediate_value(),
985            Some(&serde_json::json!(42))
986        );
987    }
988
989    // ========================================================================
990    // CompositeValue Tests
991    // ========================================================================
992
993    #[test]
994    fn test_composite_value_object_serialization() {
995        let mut fields = HashMap::new();
996        fields.insert(
997            "name".to_string(),
998            MappingValue::Reference(ReferenceValue {
999                value: "data.user.name".to_string(),
1000                type_hint: None,
1001                default: None,
1002            }),
1003        );
1004        fields.insert(
1005            "count".to_string(),
1006            MappingValue::Immediate(ImmediateValue {
1007                value: serde_json::json!(42),
1008            }),
1009        );
1010
1011        let composite = MappingValue::Composite(CompositeValue {
1012            value: CompositeInner::Object(fields),
1013        });
1014        let json = serde_json::to_value(&composite).unwrap();
1015
1016        assert_eq!(json.get("valueType").unwrap(), "composite");
1017        let value = json.get("value").unwrap();
1018        assert!(value.is_object());
1019        assert!(value.get("name").is_some());
1020        assert!(value.get("count").is_some());
1021    }
1022
1023    #[test]
1024    fn test_composite_value_array_serialization() {
1025        let elements = vec![
1026            MappingValue::Reference(ReferenceValue {
1027                value: "data.first".to_string(),
1028                type_hint: None,
1029                default: None,
1030            }),
1031            MappingValue::Immediate(ImmediateValue {
1032                value: serde_json::json!("static"),
1033            }),
1034        ];
1035
1036        let composite = MappingValue::Composite(CompositeValue {
1037            value: CompositeInner::Array(elements),
1038        });
1039        let json = serde_json::to_value(&composite).unwrap();
1040
1041        assert_eq!(json.get("valueType").unwrap(), "composite");
1042        let value = json.get("value").unwrap();
1043        assert!(value.is_array());
1044        assert_eq!(value.as_array().unwrap().len(), 2);
1045    }
1046
1047    #[test]
1048    fn test_composite_value_object_deserialization() {
1049        let json = r#"{
1050            "valueType": "composite",
1051            "value": {
1052                "userId": {"valueType": "reference", "value": "data.user.id"},
1053                "timestamp": {"valueType": "immediate", "value": 1234567890}
1054            }
1055        }"#;
1056
1057        let parsed: MappingValue = serde_json::from_str(json).unwrap();
1058        assert!(parsed.is_composite());
1059
1060        let inner = parsed.as_composite().unwrap();
1061        assert!(inner.is_object());
1062
1063        let fields = inner.as_object().unwrap();
1064        assert_eq!(fields.len(), 2);
1065        assert!(fields.get("userId").unwrap().is_reference());
1066        assert!(fields.get("timestamp").unwrap().is_immediate());
1067    }
1068
1069    #[test]
1070    fn test_composite_value_array_deserialization() {
1071        let json = r#"{
1072            "valueType": "composite",
1073            "value": [
1074                {"valueType": "reference", "value": "data.items[0]"},
1075                {"valueType": "immediate", "value": "fallback"}
1076            ]
1077        }"#;
1078
1079        let parsed: MappingValue = serde_json::from_str(json).unwrap();
1080        assert!(parsed.is_composite());
1081
1082        let inner = parsed.as_composite().unwrap();
1083        assert!(inner.is_array());
1084
1085        let elements = inner.as_array().unwrap();
1086        assert_eq!(elements.len(), 2);
1087        assert!(elements[0].is_reference());
1088        assert!(elements[1].is_immediate());
1089    }
1090
1091    #[test]
1092    fn test_nested_composite_value() {
1093        let json = r#"{
1094            "valueType": "composite",
1095            "value": {
1096                "outer": {
1097                    "valueType": "composite",
1098                    "value": {
1099                        "inner": {"valueType": "immediate", "value": "nested"}
1100                    }
1101                }
1102            }
1103        }"#;
1104
1105        let parsed: MappingValue = serde_json::from_str(json).unwrap();
1106        assert!(parsed.is_composite());
1107
1108        let outer = parsed.as_composite().unwrap().as_object().unwrap();
1109        let outer_val = outer.get("outer").unwrap();
1110        assert!(outer_val.is_composite());
1111
1112        let inner = outer_val.as_composite().unwrap().as_object().unwrap();
1113        assert!(inner.get("inner").unwrap().is_immediate());
1114    }
1115
1116    #[test]
1117    fn test_empty_composite_object() {
1118        let composite = MappingValue::Composite(CompositeValue {
1119            value: CompositeInner::Object(HashMap::new()),
1120        });
1121        let json = serde_json::to_value(&composite).unwrap();
1122        assert_eq!(json.get("valueType").unwrap(), "composite");
1123        assert!(json.get("value").unwrap().as_object().unwrap().is_empty());
1124    }
1125
1126    #[test]
1127    fn test_empty_composite_array() {
1128        let composite = MappingValue::Composite(CompositeValue {
1129            value: CompositeInner::Array(vec![]),
1130        });
1131        let json = serde_json::to_value(&composite).unwrap();
1132        assert_eq!(json.get("valueType").unwrap(), "composite");
1133        assert!(json.get("value").unwrap().as_array().unwrap().is_empty());
1134    }
1135
1136    #[test]
1137    fn test_mapping_value_collect_references() {
1138        // Simple reference
1139        let ref_val = MappingValue::Reference(ReferenceValue {
1140            value: "data.path".to_string(),
1141            type_hint: None,
1142            default: None,
1143        });
1144        assert_eq!(ref_val.collect_references(), vec!["data.path"]);
1145
1146        // Immediate has no references
1147        let imm_val = MappingValue::Immediate(ImmediateValue {
1148            value: serde_json::json!("test"),
1149        });
1150        assert!(imm_val.collect_references().is_empty());
1151
1152        // Nested composite
1153        let mut inner = HashMap::new();
1154        inner.insert(
1155            "nested".to_string(),
1156            MappingValue::Reference(ReferenceValue {
1157                value: "data.nested".to_string(),
1158                type_hint: None,
1159                default: None,
1160            }),
1161        );
1162
1163        let mut outer = HashMap::new();
1164        outer.insert(
1165            "top".to_string(),
1166            MappingValue::Reference(ReferenceValue {
1167                value: "data.top".to_string(),
1168                type_hint: None,
1169                default: None,
1170            }),
1171        );
1172        outer.insert(
1173            "inner".to_string(),
1174            MappingValue::Composite(CompositeValue {
1175                value: CompositeInner::Object(inner),
1176            }),
1177        );
1178
1179        let composite = MappingValue::Composite(CompositeValue {
1180            value: CompositeInner::Object(outer),
1181        });
1182        let refs = composite.collect_references();
1183
1184        assert_eq!(refs.len(), 2);
1185        assert!(refs.contains(&"data.top"));
1186        assert!(refs.contains(&"data.nested"));
1187    }
1188
1189    #[test]
1190    fn test_mapping_value_has_references() {
1191        let ref_val = MappingValue::Reference(ReferenceValue {
1192            value: "data.path".to_string(),
1193            type_hint: None,
1194            default: None,
1195        });
1196        assert!(ref_val.has_references());
1197
1198        let imm_val = MappingValue::Immediate(ImmediateValue {
1199            value: serde_json::json!("test"),
1200        });
1201        assert!(!imm_val.has_references());
1202
1203        // Composite with only immediates
1204        let mut fields = HashMap::new();
1205        fields.insert(
1206            "a".to_string(),
1207            MappingValue::Immediate(ImmediateValue {
1208                value: serde_json::json!(1),
1209            }),
1210        );
1211        let comp_no_refs = MappingValue::Composite(CompositeValue {
1212            value: CompositeInner::Object(fields),
1213        });
1214        assert!(!comp_no_refs.has_references());
1215
1216        // Composite with references
1217        let mut fields_with_refs = HashMap::new();
1218        fields_with_refs.insert(
1219            "a".to_string(),
1220            MappingValue::Reference(ReferenceValue {
1221                value: "data.a".to_string(),
1222                type_hint: None,
1223                default: None,
1224            }),
1225        );
1226        let comp_with_refs = MappingValue::Composite(CompositeValue {
1227            value: CompositeInner::Object(fields_with_refs),
1228        });
1229        assert!(comp_with_refs.has_references());
1230    }
1231
1232    #[test]
1233    fn test_mapping_value_is_composite() {
1234        let ref_val = MappingValue::Reference(ReferenceValue {
1235            value: "data.field".to_string(),
1236            type_hint: None,
1237            default: None,
1238        });
1239        let imm_val = MappingValue::Immediate(ImmediateValue {
1240            value: serde_json::json!("static"),
1241        });
1242        let comp_val = MappingValue::Composite(CompositeValue {
1243            value: CompositeInner::Object(HashMap::new()),
1244        });
1245
1246        assert!(!ref_val.is_composite());
1247        assert!(!imm_val.is_composite());
1248        assert!(comp_val.is_composite());
1249    }
1250
1251    #[test]
1252    fn test_composite_value_round_trip() {
1253        // Object composite
1254        let mut fields = HashMap::new();
1255        fields.insert(
1256            "key".to_string(),
1257            MappingValue::Reference(ReferenceValue {
1258                value: "data.key".to_string(),
1259                type_hint: None,
1260                default: None,
1261            }),
1262        );
1263        let original = MappingValue::Composite(CompositeValue {
1264            value: CompositeInner::Object(fields),
1265        });
1266        let json = serde_json::to_string(&original).unwrap();
1267        let parsed: MappingValue = serde_json::from_str(&json).unwrap();
1268        assert!(parsed.is_composite());
1269        assert!(parsed.as_composite().unwrap().is_object());
1270
1271        // Array composite
1272        let elements = vec![MappingValue::Immediate(ImmediateValue {
1273            value: serde_json::json!("test"),
1274        })];
1275        let original_arr = MappingValue::Composite(CompositeValue {
1276            value: CompositeInner::Array(elements),
1277        });
1278        let json_arr = serde_json::to_string(&original_arr).unwrap();
1279        let parsed_arr: MappingValue = serde_json::from_str(&json_arr).unwrap();
1280        assert!(parsed_arr.is_composite());
1281        assert!(parsed_arr.as_composite().unwrap().is_array());
1282    }
1283
1284    // ========================================================================
1285    // LogLevel and LogStep Tests
1286    // ========================================================================
1287
1288    #[test]
1289    fn test_log_level_serialization() {
1290        assert_eq!(
1291            serde_json::to_string(&LogLevel::Debug).unwrap(),
1292            "\"debug\""
1293        );
1294        assert_eq!(serde_json::to_string(&LogLevel::Info).unwrap(), "\"info\"");
1295        assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), "\"warn\"");
1296        assert_eq!(
1297            serde_json::to_string(&LogLevel::Error).unwrap(),
1298            "\"error\""
1299        );
1300    }
1301
1302    #[test]
1303    fn test_log_step_serialization() {
1304        let step = LogStep {
1305            id: "log1".to_string(),
1306            name: Some("Debug Log".to_string()),
1307            level: LogLevel::Debug,
1308            message: "Processing item".to_string(),
1309            context: None,
1310        };
1311
1312        let json = serde_json::to_value(&step).unwrap();
1313        assert_eq!(json.get("id").unwrap(), "log1");
1314        assert_eq!(json.get("level").unwrap(), "debug");
1315        assert_eq!(json.get("message").unwrap(), "Processing item");
1316    }
1317
1318    // ========================================================================
1319    // WhileStep Tests
1320    // ========================================================================
1321
1322    #[test]
1323    fn test_while_step_serialization() {
1324        let step = WhileStep {
1325            id: "while1".to_string(),
1326            name: Some("Retry Loop".to_string()),
1327            condition: ConditionExpression::Value(MappingValue::Reference(ReferenceValue {
1328                value: "data.retry".to_string(),
1329                type_hint: Some(ValueType::Boolean),
1330                default: None,
1331            })),
1332            subgraph: Box::new(ExecutionGraph {
1333                name: None,
1334                description: None,
1335                steps: HashMap::new(),
1336                entry_point: "start".to_string(),
1337                execution_plan: vec![],
1338                variables: HashMap::new(),
1339                input_schema: HashMap::new(),
1340                output_schema: HashMap::new(),
1341                notes: None,
1342                nodes: None,
1343                edges: None,
1344            }),
1345            config: Some(WhileConfig {
1346                max_iterations: Some(10),
1347                timeout: Some(5000),
1348            }),
1349        };
1350
1351        let json = serde_json::to_value(&step).unwrap();
1352        assert_eq!(json.get("id").unwrap(), "while1");
1353        let config = json.get("config").unwrap();
1354        assert_eq!(config.get("maxIterations").unwrap(), 10);
1355        // WhileConfig uses "timeout" not "timeoutMs"
1356        assert_eq!(config.get("timeout").unwrap(), 5000);
1357    }
1358
1359    // ========================================================================
1360    // ConnectionStep Tests
1361    // ========================================================================
1362
1363    #[test]
1364    fn test_connection_step_serialization() {
1365        let step = ConnectionStep {
1366            id: "conn1".to_string(),
1367            name: Some("API Connection".to_string()),
1368            connection_id: "my-api-key".to_string(),
1369            integration_id: "http_bearer".to_string(),
1370        };
1371
1372        let json = serde_json::to_value(&step).unwrap();
1373        assert_eq!(json.get("id").unwrap(), "conn1");
1374        assert_eq!(json.get("connectionId").unwrap(), "my-api-key");
1375        assert_eq!(json.get("integrationId").unwrap(), "http_bearer");
1376    }
1377}