| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501 |
- 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))
- }
- })
- }
|