package workflow import ( "context" "fmt" "strings" "testing" ) func newTestEvaluator() *ExpressionEvaluator { ctx := &ExecutionContext{ Variables: map[string]interface{}{ "$name": "Alice", "$age": 30, "$active": true, "$score": 85.5, "$items": []interface{}{"a", "b", "c"}, "$empty": "", "$zero": 0, "$nothing": nil, "$user": map[string]interface{}{ "name": "Bob", "age": 25, "address": map[string]interface{}{ "city": "NYC", "zipcode": "10001", }, }, "$numbers": []interface{}{1, 2, 3, 4, 5}, }, LocalVars: map[string]interface{}{ "_result": map[string]interface{}{ "status": "success", "data": 42, }, "_item": "current", "_index": 2, }, } // Use setter method for system variables ctx.SetSystemVar("SYSVAR.workflowId", "wf-123") ctx.SetSystemVar("SYSVAR.timestamp", 1234567890) return NewExpressionEvaluator(ctx) } func TestEvaluateValue(t *testing.T) { eval := newTestEvaluator() t.Run("NonStringTypes", func(t *testing.T) { // Non-string types returned as-is tests := []struct { name string input interface{} expected interface{} }{ {"int", 42, 42}, {"float", 3.14, 3.14}, {"bool true", true, true}, {"bool false", false, false}, {"nil", nil, nil}, {"slice", []interface{}{"a", "b"}, nil}, // compared separately {"map", map[string]interface{}{"k": "v"}, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.EvaluateValue(tt.input) if err != nil { t.Fatalf("unexpected error: %v", err) } if tt.name == "slice" || tt.name == "map" { // Just check it's returned as-is (same pointer) if result == nil { t.Errorf("expected non-nil result") } } else if result != tt.expected { t.Errorf("expected %v (%T), got %v (%T)", tt.expected, tt.expected, result, result) } }) } }) t.Run("LiteralStrings", func(t *testing.T) { // Strings without = prefix are literal tests := []struct { name string input string expected string }{ {"plain text", "hello world", "hello world"}, {"empty string", "", ""}, {"url", "https://example.com", "https://example.com"}, {"path", "/var/log/app.log", "/var/log/app.log"}, {"dollar no prefix", "$name", "$name"}, {"number string", "42", "42"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.EvaluateValue(tt.input) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %q, got %v (%T)", tt.expected, result, result) } }) } }) t.Run("ExpressionStrings", func(t *testing.T) { // Strings with = prefix evaluate the rest as expression tests := []struct { name string input string expected interface{} }{ {"variable ref", "=$name", "Alice"}, {"variable int", "=$age", 30}, {"variable bool", "=$active", true}, {"expression", "=$age + 5", float64(35)}, {"comparison", "=$age > 20", true}, {"nested path", "=$user.name", "Bob"}, {"string literal expr", `="hello"`, "hello"}, {"numeric literal", "=42", int64(42)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.EvaluateValue(tt.input) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v (%T), got %v (%T)", tt.expected, tt.expected, result, result) } }) } }) t.Run("EscapedEquals", func(t *testing.T) { // Strings with == prefix strip one = and return as literal tests := []struct { name string input string expected string }{ {"escaped equals", "==something", "=something"}, {"escaped var-like", "==$name", "=$name"}, {"escaped expression", "==$age + 5", "=$age + 5"}, {"triple equals", "===foo", "==foo"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.EvaluateValue(tt.input) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %q, got %v (%T)", tt.expected, result, result) } }) } }) } func TestEvaluateLiterals(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected interface{} }{ {"empty string", "", nil}, {"string double quotes", `"hello"`, "hello"}, {"string single quotes", `'world'`, "world"}, {"string with spaces", `"hello world"`, "hello world"}, {"boolean true", "true", true}, {"boolean false", "false", false}, {"null", "null", nil}, {"nil", "nil", nil}, {"integer", "42", int64(42)}, {"negative integer", "-5", int64(-5)}, {"float", "3.14", 3.14}, {"negative float", "-2.5", -2.5}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v (%T), got %v (%T)", tt.expected, tt.expected, result, result) } }) } } func TestEvaluateGlobalVariables(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected interface{} }{ {"string var", "$name", "Alice"}, {"int var", "$age", 30}, {"bool var", "$active", true}, {"float var", "$score", 85.5}, {"nil var", "$nothing", nil}, {"undefined var", "$undefined", nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateNestedPaths(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected interface{} }{ {"nested field", "$user.name", "Bob"}, {"nested int field", "$user.age", 25}, {"deep nested field", "$user.address.city", "NYC"}, {"array index", "$items[0]", "a"}, {"array index 1", "$items[1]", "b"}, {"array index 2", "$items[2]", "c"}, {"numeric array", "$numbers[0]", 1}, {"numeric array last", "$numbers[4]", 5}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateSystemVariables(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected interface{} }{ {"workflow id", "SYSVAR.workflowId", "wf-123"}, {"timestamp", "SYSVAR.timestamp", 1234567890}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateLocalVariables(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected interface{} }{ {"local item", "_item", "current"}, {"local index", "_index", 2}, {"local result status", "_result.status", "success"}, {"local result data", "_result.data", 42}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateComparisons(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected bool }{ // Equality {"int equals true", "$age == 30", true}, {"int equals false", "$age == 25", false}, {"string equals true", "$name == \"Alice\"", true}, {"string equals false", "$name == \"Bob\"", false}, {"bool equals true", "$active == true", true}, {"bool equals false", "$active == false", false}, // Inequality {"not equals true", "$age != 25", true}, {"not equals false", "$age != 30", false}, // Greater than {"greater than true", "$age > 25", true}, {"greater than false", "$age > 35", false}, {"greater than equal", "$age > 30", false}, // Greater than or equal {"gte true greater", "$age >= 25", true}, {"gte true equal", "$age >= 30", true}, {"gte false", "$age >= 35", false}, // Less than {"less than true", "$age < 35", true}, {"less than false", "$age < 25", false}, {"less than equal", "$age < 30", false}, // Less than or equal {"lte true less", "$age <= 35", true}, {"lte true equal", "$age <= 30", true}, {"lte false", "$age <= 25", false}, // Float comparisons {"float greater", "$score > 80", true}, {"float less", "$score < 90", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateLogicalOperators(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected bool }{ // AND {"and both true", "$active == true && $age > 20", true}, {"and first false", "$active == false && $age > 20", false}, {"and second false", "$active == true && $age > 40", false}, {"and both false", "$active == false && $age > 40", false}, // OR {"or both true", "$active == true || $age > 40", true}, {"or first true", "$active == true || $age > 40", true}, {"or second true", "$active == false || $age > 20", true}, {"or both false", "$active == false || $age > 40", false}, // Negation {"not true", "!false", true}, {"not false", "!true", false}, {"not var true", "!$active", false}, // Combined {"complex and or", "$age > 20 && $active == true || $score < 50", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateArithmetic(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected float64 }{ {"addition", "10 + 5", 15}, {"subtraction", "10 - 3", 7}, {"multiplication", "4 * 3", 12}, {"division", "15 / 3", 5}, {"var addition", "$age + 5", 35}, {"var subtraction", "$age - 10", 20}, {"float arithmetic", "$score + 4.5", 90}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateStringConcatenation(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string expr string expected string }{ {"concat strings", `"hello" + " world"`, "hello world"}, {"concat var and string", `$name + " Smith"`, "Alice Smith"}, {"concat string and var", `"User: " + $name`, "User: Alice"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval.Evaluate(tt.expr) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestEvaluateDivisionByZero(t *testing.T) { eval := newTestEvaluator() _, err := eval.Evaluate("10 / 0") if err == nil { t.Fatal("expected division by zero error") } } func TestEvaluateArrayIndexOutOfRange(t *testing.T) { eval := newTestEvaluator() _, err := eval.Evaluate("$items[10]") if err == nil { t.Fatal("expected index out of range error") } } func TestSetVariable(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string path string value interface{} checkVar string expected interface{} }{ {"set simple var", "$newVar", "test value", "$newVar", "test value"}, {"set nested field", "$user.email", "bob@test.com", "$user.email", "bob@test.com"}, {"set int value", "$count", 100, "$count", 100}, {"set bool value", "$flag", false, "$flag", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := eval.SetVariable(tt.path, tt.value) if err != nil { t.Fatalf("unexpected error: %v", err) } result, err := eval.Evaluate(tt.checkVar) if err != nil { t.Fatalf("unexpected error reading back: %v", err) } if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestSetVariableDeepSet(t *testing.T) { t.Run("array index then field - auto-create from scratch", func(t *testing.T) { // Simulates: $generated[0].name = "hello" with $generated not existing ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) err := eval.SetVariable("$generated[0].name", "hello") if err != nil { t.Fatalf("unexpected error: %v", err) } result, err := eval.Evaluate("$generated[0].name") if err != nil { t.Fatalf("unexpected error reading back: %v", err) } if result != "hello" { t.Errorf("expected 'hello', got %v", result) } }) t.Run("array index then field - auto-grow existing array", func(t *testing.T) { // Simulates: $generated[2].name = "third" when $generated has only 1 element ctx := &ExecutionContext{ Variables: map[string]interface{}{ "$generated": []interface{}{ map[string]interface{}{"name": "first"}, }, }, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) err := eval.SetVariable("$generated[2].name", "third") if err != nil { t.Fatalf("unexpected error: %v", err) } result, err := eval.Evaluate("$generated[2].name") if err != nil { t.Fatalf("unexpected error reading back: %v", err) } if result != "third" { t.Errorf("expected 'third', got %v", result) } // First element should be untouched first, err := eval.Evaluate("$generated[0].name") if err != nil { t.Fatalf("unexpected error: %v", err) } if first != "first" { t.Errorf("expected 'first', got %v", first) } }) t.Run("multiple fields on same array element", func(t *testing.T) { // Simulates loop iteration writing: $generated[1].name and $generated[1].code ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) if err := eval.SetVariable("$generated[1].name", "component"); err != nil { t.Fatalf("unexpected error: %v", err) } if err := eval.SetVariable("$generated[1].code", "export default {}"); err != nil { t.Fatalf("unexpected error: %v", err) } name, err := eval.Evaluate("$generated[1].name") if err != nil { t.Fatalf("unexpected error: %v", err) } if name != "component" { t.Errorf("expected 'component', got %v", name) } code, err := eval.Evaluate("$generated[1].code") if err != nil { t.Fatalf("unexpected error: %v", err) } if code != "export default {}" { t.Errorf("expected 'export default {}', got %v", code) } }) t.Run("nested map auto-creation", func(t *testing.T) { // $config.database.host = "localhost" ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) err := eval.SetVariable("$config.database.host", "localhost") if err != nil { t.Fatalf("unexpected error: %v", err) } result, err := eval.Evaluate("$config.database.host") if err != nil { t.Fatalf("unexpected error: %v", err) } if result != "localhost" { t.Errorf("expected 'localhost', got %v", result) } }) } func TestSetVariableErrors(t *testing.T) { eval := newTestEvaluator() tests := []struct { name string path string value interface{} }{ {"empty path", "", "value"}, {"local var", "_local", "value"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := eval.SetVariable(tt.path, tt.value) if err == nil { t.Fatal("expected error") } }) } } func TestToBool(t *testing.T) { tests := []struct { name string value interface{} expected bool }{ {"nil", nil, false}, {"true", true, true}, {"false", false, false}, {"zero int", 0, false}, {"nonzero int", 5, true}, {"zero float", 0.0, false}, {"nonzero float", 3.14, true}, {"empty string", "", false}, {"nonempty string", "hello", true}, {"map", map[string]interface{}{}, true}, {"slice", []interface{}{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := toBool(tt.value) if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestToFloat64(t *testing.T) { tests := []struct { name string value interface{} expected float64 }{ {"nil", nil, 0}, {"int", 42, 42}, {"int64", int64(100), 100}, {"float64", 3.14, 3.14}, {"float32", float32(2.5), 2.5}, {"string number", "123", 123}, {"string float", "45.67", 45.67}, {"invalid string", "abc", 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := toFloat64(tt.value) if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestToString(t *testing.T) { tests := []struct { name string value interface{} expected string }{ {"nil", nil, ""}, {"string", "hello", "hello"}, {"int", 42, "42"}, {"float", 3.14, "3.14"}, {"bool true", true, "true"}, {"bool false", false, "false"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := toString(tt.value) if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestIsString(t *testing.T) { tests := []struct { name string value interface{} expected bool }{ {"string", "hello", true}, {"int", 42, false}, {"nil", nil, false}, {"bool", true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isString(tt.value) if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestSetVariableObjectTypeConversionIntegration(t *testing.T) { t.Run("workflow with OBJECT type variable", func(t *testing.T) { // Create a workflow with an OBJECT type variable in the registry workflow := &Workflow{ Version: "3.6", Name: "TestOBJECTConversion", Registry: Registry{ Vars: []string{ "$config(OBJECT)", "$name(STRING)", }, }, Steps: []Step{ { ID: "Set_Config", Target: "$config", Value: `{"host": "localhost", "port": 8080, "enabled": true}`, Next: "Stop_End", }, { ID: "Stop_End", }, }, } // Create engine and execute engine, err := NewEngine(workflow) if err != nil { t.Fatalf("failed to create engine: %v", err) } ctx := context.Background() adapters := &Adapters{} result, err := engine.Execute(ctx, map[string]interface{}{}, adapters) if err != nil { t.Fatalf("failed to execute workflow: %v", err) } // Wait for completion for event := range result.RunEventStream { if event.Type == RunEventWorkflowDone { break } } // Verify the config was converted to a map config, ok := result.Context.Variables["$config"] if !ok { t.Fatal("$config variable not set") } configMap, ok := config.(map[string]interface{}) if !ok { t.Fatalf("expected $config to be map[string]interface{}, got %T", config) } if configMap["host"] != "localhost" { t.Errorf("expected host=localhost, got %v", configMap["host"]) } if configMap["port"] != float64(8080) { t.Errorf("expected port=8080, got %v", configMap["port"]) } if configMap["enabled"] != true { t.Errorf("expected enabled=true, got %v", configMap["enabled"]) } }) t.Run("workflow with invalid JSON for OBJECT type should fail", func(t *testing.T) { workflow := &Workflow{ Version: "3.6", Name: "TestOBJECTConversionError", Registry: Registry{ Vars: []string{ "$config(OBJECT)", }, }, Steps: []Step{ { ID: "Set_Config", Target: "$config", Value: "invalid json string", Next: "Stop_End", }, { ID: "Stop_End", }, }, } engine, err := NewEngine(workflow) if err != nil { t.Fatalf("failed to create engine: %v", err) } ctx := context.Background() adapters := &Adapters{} result, err := engine.Execute(ctx, map[string]interface{}{}, adapters) if err != nil { t.Fatalf("failed to execute workflow: %v", err) } // Wait for failure foundError := false for event := range result.RunEventStream { if event.Type == RunEventWorkflowFailed { if strings.Contains(fmt.Sprintf("%v", event.Payload), "failed to convert string to OBJECT") { foundError = true } break } } if !foundError { t.Error("expected workflow to fail with conversion error") } }) } func TestParamReferences(t *testing.T) { t.Run("read parameter value", func(t *testing.T) { ctx := &ExecutionContext{ Params: map[string]interface{}{ "userId": "user123", "limit": 10, }, Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) // Evaluate parameter reference result, err := eval.Evaluate("userId") if err != nil { t.Fatalf("unexpected error: %v", err) } if result != "user123" { t.Errorf("expected 'user123', got %v", result) } // Evaluate numeric parameter result, err = eval.Evaluate("limit") if err != nil { t.Fatalf("unexpected error: %v", err) } if result != 10 { t.Errorf("expected 10, got %v", result) } }) t.Run("use parameter in expression", func(t *testing.T) { ctx := &ExecutionContext{ Params: map[string]interface{}{ "maxCount": 100, }, Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) // Use parameter in arithmetic result, err := eval.Evaluate("maxCount + 50") if err != nil { t.Fatalf("unexpected error: %v", err) } if result != float64(150) { t.Errorf("expected 150, got %v", result) } }) t.Run("access nested parameter fields", func(t *testing.T) { ctx := &ExecutionContext{ Params: map[string]interface{}{ "config": map[string]interface{}{ "host": "localhost", "port": 8080, }, }, Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) // Access nested field result, err := eval.Evaluate("config.host") if err != nil { t.Fatalf("unexpected error: %v", err) } if result != "localhost" { t.Errorf("expected 'localhost', got %v", result) } }) t.Run("cannot modify parameter", func(t *testing.T) { ctx := &ExecutionContext{ Params: map[string]interface{}{ "userId": "user123", }, Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) // Try to set a parameter value err := eval.SetVariable("userId", "newValue") if err == nil { t.Fatal("expected error when trying to modify parameter") } if !strings.Contains(err.Error(), "cannot modify parameter") { t.Errorf("expected 'cannot modify parameter' error, got: %v", err) } // Verify parameter unchanged if ctx.Params["userId"] != "user123" { t.Errorf("parameter was modified") } }) t.Run("error on undefined parameter", func(t *testing.T) { ctx := &ExecutionContext{ Params: map[string]interface{}{}, Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, } eval := NewExpressionEvaluator(ctx) // Try to access undefined parameter _, err := eval.Evaluate("undefinedParam") if err == nil { t.Fatal("expected error for undefined parameter") } if !strings.Contains(err.Error(), "parameter not found") { t.Errorf("expected 'parameter not found' error, got: %v", err) } }) } func TestParamIntegration(t *testing.T) { t.Run("OBJECT parameter type conversion on init", func(t *testing.T) { workflow := &Workflow{ Version: "3.6", Name: "TestParamOBJECTConversion", Registry: Registry{ Params: []string{ "config(OBJECT)", }, Vars: []string{ "$result(STRING)", }, }, Steps: []Step{ { ID: "Set_Result", Target: "$result", Value: "=config.host", Next: "Stop_End", }, { ID: "Stop_End", }, }, } engine, err := NewEngine(workflow) if err != nil { t.Fatalf("failed to create engine: %v", err) } ctx := context.Background() adapters := &Adapters{} // Pass config as JSON string - should be converted to map result, err := engine.Execute(ctx, map[string]interface{}{ "config": `{"host": "api.example.com", "port": 443}`, }, adapters) if err != nil { t.Fatalf("failed to execute workflow: %v", err) } // Wait for completion for event := range result.RunEventStream { if event.Type == RunEventWorkflowDone { break } } // Verify parameter was converted to map config, ok := result.Context.Params["config"] if !ok { t.Fatal("config parameter not set") } configMap, ok := config.(map[string]interface{}) if !ok { t.Fatalf("expected config to be map[string]interface{}, got %T", config) } if configMap["host"] != "api.example.com" { t.Errorf("expected host=api.example.com, got %v", configMap["host"]) } // Verify the nested access worked in expression if result.Context.Variables["$result"] != "api.example.com" { t.Errorf("expected $result=api.example.com, got %v", result.Context.Variables["$result"]) } }) t.Run("workflow with parameters", func(t *testing.T) { workflow := &Workflow{ Version: "3.6", Name: "TestParams", Registry: Registry{ Params: []string{ "userId(STRING)", "maxLimit(INT)", }, Vars: []string{ "$result(STRING)", }, }, Steps: []Step{ { ID: "Set_Result", Target: "$result", Value: "=userId", Next: "Stop_End", }, { ID: "Stop_End", }, }, } engine, err := NewEngine(workflow) if err != nil { t.Fatalf("failed to create engine: %v", err) } ctx := context.Background() adapters := &Adapters{} // Pass parameters through initialVars result, err := engine.Execute(ctx, map[string]interface{}{ "userId": "user123", "maxLimit": 100, }, adapters) if err != nil { t.Fatalf("failed to execute workflow: %v", err) } // Wait for completion for event := range result.RunEventStream { if event.Type == RunEventWorkflowDone { break } } // Verify parameter was accessible and result was set if result.Context.Params["userId"] != "user123" { t.Errorf("expected userId=user123, got %v", result.Context.Params["userId"]) } if result.Context.Variables["$result"] != "user123" { t.Errorf("expected $result=user123, got %v", result.Context.Variables["$result"]) } }) t.Run("invalid JSON for OBJECT parameter on init", func(t *testing.T) { workflow := &Workflow{ Version: "3.6", Name: "TestParamOBJECTConversionError", Registry: Registry{ Params: []string{ "config(OBJECT)", }, }, Steps: []Step{ { ID: "Stop_End", }, }, } engine, err := NewEngine(workflow) if err != nil { t.Fatalf("failed to create engine: %v", err) } ctx := context.Background() adapters := &Adapters{} // Pass invalid JSON for OBJECT parameter _, err = engine.Execute(ctx, map[string]interface{}{ "config": "not valid json", }, adapters) if err == nil { t.Fatal("expected error for invalid JSON in OBJECT parameter") } if !strings.Contains(err.Error(), "failed to convert parameter config to OBJECT") { t.Errorf("expected conversion error, got: %v", err) } }) t.Run("workflow cannot modify parameter", func(t *testing.T) { workflow := &Workflow{ Version: "3.6", Name: "TestParamReadOnly", Registry: Registry{ Params: []string{ "userId(STRING)", }, }, Steps: []Step{ { ID: "Set_Param", Target: "userId", Value: "newValue", Next: "Stop_End", }, { ID: "Stop_End", }, }, } engine, err := NewEngine(workflow) if err != nil { t.Fatalf("failed to create engine: %v", err) } ctx := context.Background() adapters := &Adapters{} result, err := engine.Execute(ctx, map[string]interface{}{ "userId": "user123", }, adapters) if err != nil { t.Fatalf("failed to execute workflow: %v", err) } // Wait for failure foundError := false for event := range result.RunEventStream { if event.Type == RunEventWorkflowFailed { if strings.Contains(fmt.Sprintf("%v", event.Payload), "cannot modify parameter") { foundError = true } break } } if !foundError { t.Error("expected workflow to fail when trying to modify parameter") } }) } func TestSetVariableObjectTypeConversion(t *testing.T) { t.Run("convert string to map for OBJECT type", func(t *testing.T) { ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, VarTypes: map[string]string{ "$config": "OBJECT", }, } eval := NewExpressionEvaluator(ctx) // Set a string value to an OBJECT type variable jsonStr := `{"host": "localhost", "port": 8080}` err := eval.SetVariable("$config", jsonStr) if err != nil { t.Fatalf("unexpected error: %v", err) } // Verify it was converted to a map result, ok := ctx.Variables["$config"] if !ok { t.Fatal("variable not set") } resultMap, ok := result.(map[string]interface{}) if !ok { t.Fatalf("expected map[string]interface{}, got %T", result) } if resultMap["host"] != "localhost" { t.Errorf("expected host=localhost, got %v", resultMap["host"]) } if resultMap["port"] != float64(8080) { t.Errorf("expected port=8080, got %v", resultMap["port"]) } }) t.Run("error on invalid JSON for OBJECT type", func(t *testing.T) { ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, VarTypes: map[string]string{ "$config": "OBJECT", }, } eval := NewExpressionEvaluator(ctx) // Try to set an invalid JSON string err := eval.SetVariable("$config", "not valid json") if err == nil { t.Fatal("expected error for invalid JSON") } if !strings.Contains(err.Error(), "failed to convert string to OBJECT") { t.Errorf("expected conversion error, got: %v", err) } }) t.Run("no conversion for non-string values", func(t *testing.T) { ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, VarTypes: map[string]string{ "$config": "OBJECT", }, } eval := NewExpressionEvaluator(ctx) // Set a map directly (no conversion needed) mapValue := map[string]interface{}{"key": "value"} err := eval.SetVariable("$config", mapValue) if err != nil { t.Fatalf("unexpected error: %v", err) } // Verify it's the same map result, ok := ctx.Variables["$config"] if !ok { t.Fatal("variable not set") } resultMap, ok := result.(map[string]interface{}) if !ok { t.Fatalf("expected map[string]interface{}, got %T", result) } if resultMap["key"] != "value" { t.Errorf("expected key=value, got %v", resultMap["key"]) } }) t.Run("no conversion for non-OBJECT types", func(t *testing.T) { ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, VarTypes: map[string]string{ "$name": "STRING", }, } eval := NewExpressionEvaluator(ctx) // Set a string value to a STRING type variable (no conversion) err := eval.SetVariable("$name", `{"not": "converted"}`) if err != nil { t.Fatalf("unexpected error: %v", err) } // Verify it's still a string result, ok := ctx.Variables["$name"] if !ok { t.Fatal("variable not set") } if _, ok := result.(string); !ok { t.Fatalf("expected string, got %T", result) } }) t.Run("no conversion when VarTypes is nil", func(t *testing.T) { ctx := &ExecutionContext{ Variables: map[string]interface{}{}, LocalVars: map[string]interface{}{}, VarTypes: nil, } eval := NewExpressionEvaluator(ctx) // Set a string value (no conversion without type info) err := eval.SetVariable("$config", `{"key": "value"}`) if err != nil { t.Fatalf("unexpected error: %v", err) } // Verify it's still a string result, ok := ctx.Variables["$config"] if !ok { t.Fatal("variable not set") } if _, ok := result.(string); !ok { t.Fatalf("expected string, got %T", result) } }) } func TestEvaluateJSONLiterals(t *testing.T) { eval := newTestEvaluator() t.Run("array of objects", func(t *testing.T) { result, err := eval.Evaluate(`[{"label":"Alpha"},{"label":"Beta"},{"label":"Gamma"}]`) if err != nil { t.Fatalf("unexpected error: %v", err) } arr, ok := result.([]interface{}) if !ok { t.Fatalf("expected []interface{}, got %T", result) } if len(arr) != 3 { t.Fatalf("expected 3 elements, got %d", len(arr)) } first, ok := arr[0].(map[string]interface{}) if !ok { t.Fatalf("expected map[string]interface{}, got %T", arr[0]) } if first["label"] != "Alpha" { t.Errorf("expected Alpha, got %v", first["label"]) } }) t.Run("simple array", func(t *testing.T) { result, err := eval.Evaluate(`[1, 2, 3]`) if err != nil { t.Fatalf("unexpected error: %v", err) } arr, ok := result.([]interface{}) if !ok { t.Fatalf("expected []interface{}, got %T", result) } if len(arr) != 3 { t.Fatalf("expected 3 elements, got %d", len(arr)) } if arr[0] != float64(1) { t.Errorf("expected 1, got %v", arr[0]) } }) t.Run("object literal", func(t *testing.T) { result, err := eval.Evaluate(`{"key":"value","count":42}`) if err != nil { t.Fatalf("unexpected error: %v", err) } obj, ok := result.(map[string]interface{}) if !ok { t.Fatalf("expected map[string]interface{}, got %T", result) } if obj["key"] != "value" { t.Errorf("expected 'value', got %v", obj["key"]) } if obj["count"] != float64(42) { t.Errorf("expected 42, got %v", obj["count"]) } }) t.Run("empty array", func(t *testing.T) { result, err := eval.Evaluate(`[]`) if err != nil { t.Fatalf("unexpected error: %v", err) } arr, ok := result.([]interface{}) if !ok { t.Fatalf("expected []interface{}, got %T", result) } if len(arr) != 0 { t.Errorf("expected empty array, got %d elements", len(arr)) } }) t.Run("empty object", func(t *testing.T) { result, err := eval.Evaluate(`{}`) if err != nil { t.Fatalf("unexpected error: %v", err) } obj, ok := result.(map[string]interface{}) if !ok { t.Fatalf("expected map[string]interface{}, got %T", result) } if len(obj) != 0 { t.Errorf("expected empty object, got %d keys", len(obj)) } }) t.Run("via EvaluateValue with = prefix", func(t *testing.T) { result, err := eval.EvaluateValue(`=[{"label":"Alpha"},{"label":"Beta"},{"label":"Gamma"}]`) if err != nil { t.Fatalf("unexpected error: %v", err) } arr, ok := result.([]interface{}) if !ok { t.Fatalf("expected []interface{}, got %T", result) } if len(arr) != 3 { t.Fatalf("expected 3 elements, got %d", len(arr)) } }) t.Run("nested array of objects", func(t *testing.T) { result, err := eval.Evaluate(`[{"items":[1,2,3]},{"items":[4,5,6]}]`) if err != nil { t.Fatalf("unexpected error: %v", err) } arr, ok := result.([]interface{}) if !ok { t.Fatalf("expected []interface{}, got %T", result) } if len(arr) != 2 { t.Fatalf("expected 2 elements, got %d", len(arr)) } first, ok := arr[0].(map[string]interface{}) if !ok { t.Fatalf("expected map, got %T", arr[0]) } items, ok := first["items"].([]interface{}) if !ok { t.Fatalf("expected []interface{}, got %T", first["items"]) } if len(items) != 3 { t.Errorf("expected 3 items, got %d", len(items)) } }) }