decision_agent 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/dmn/adapter.rb +135 -0
  5. data/lib/decision_agent/dmn/cache.rb +306 -0
  6. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  7. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  8. data/lib/decision_agent/dmn/errors.rb +30 -0
  9. data/lib/decision_agent/dmn/exporter.rb +217 -0
  10. data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
  11. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  12. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  13. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  14. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  15. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  16. data/lib/decision_agent/dmn/importer.rb +77 -0
  17. data/lib/decision_agent/dmn/model.rb +197 -0
  18. data/lib/decision_agent/dmn/parser.rb +191 -0
  19. data/lib/decision_agent/dmn/testing.rb +333 -0
  20. data/lib/decision_agent/dmn/validator.rb +315 -0
  21. data/lib/decision_agent/dmn/versioning.rb +229 -0
  22. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  23. data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
  24. data/lib/decision_agent/dsl/schema_validator.rb +2 -1
  25. data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
  26. data/lib/decision_agent/version.rb +1 -1
  27. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  28. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  29. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  30. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  31. data/lib/decision_agent/web/public/index.html +3 -0
  32. data/lib/decision_agent/web/public/styles.css +21 -0
  33. data/lib/decision_agent/web/server.rb +465 -0
  34. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  35. data/spec/auth/rbac_adapter_spec.rb +228 -0
  36. data/spec/dmn/decision_graph_spec.rb +282 -0
  37. data/spec/dmn/decision_tree_spec.rb +203 -0
  38. data/spec/dmn/feel/errors_spec.rb +18 -0
  39. data/spec/dmn/feel/functions_spec.rb +400 -0
  40. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  41. data/spec/dmn/feel/types_spec.rb +176 -0
  42. data/spec/dmn/feel_parser_spec.rb +489 -0
  43. data/spec/dmn/hit_policy_spec.rb +202 -0
  44. data/spec/dmn/integration_spec.rb +226 -0
  45. data/spec/examples.txt +1846 -1570
  46. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  47. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  48. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  49. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  50. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  51. data/spec/performance_optimizations_spec.rb +10 -3
  52. data/spec/thread_safety_spec.rb +10 -2
  53. data/spec/web_ui_rack_spec.rb +294 -0
  54. metadata +65 -1
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "decision_agent/dmn/decision_tree"
5
+
6
+ RSpec.describe DecisionAgent::Dmn::DecisionTree do
7
+ describe "basic tree structure" do
8
+ let(:tree) do
9
+ described_class.new(id: "tree1", name: "Loan Approval Tree")
10
+ end
11
+
12
+ it "creates a tree with root node" do
13
+ expect(tree.root).to be_a(DecisionAgent::Dmn::TreeNode)
14
+ expect(tree.root.id).to eq("root")
15
+ end
16
+
17
+ it "allows adding children to nodes" do
18
+ child1 = DecisionAgent::Dmn::TreeNode.new(id: "child1", label: "Check Age")
19
+ tree.root.add_child(child1)
20
+
21
+ expect(tree.root.children).to include(child1)
22
+ expect(child1.parent).to eq(tree.root)
23
+ end
24
+ end
25
+
26
+ describe "tree evaluation" do
27
+ let(:tree) do
28
+ # Build a simple decision tree for loan approval
29
+ tree = described_class.new(id: "loan_tree", name: "Loan Approval")
30
+
31
+ # Root node
32
+ root = tree.root
33
+
34
+ # First level - check age
35
+ age_check = DecisionAgent::Dmn::TreeNode.new(
36
+ id: "age_check",
37
+ label: "Age >= 18?",
38
+ condition: "age >= 18"
39
+ )
40
+ root.add_child(age_check)
41
+
42
+ # Second level under age check - check credit score
43
+ good_credit = DecisionAgent::Dmn::TreeNode.new(
44
+ id: "good_credit",
45
+ label: "Credit Score >= 650?",
46
+ condition: "credit_score >= 650"
47
+ )
48
+ age_check.add_child(good_credit)
49
+
50
+ # Leaf nodes - decisions
51
+ approved = DecisionAgent::Dmn::TreeNode.new(
52
+ id: "approved",
53
+ label: "Approved",
54
+ decision: "Approved"
55
+ )
56
+ good_credit.add_child(approved)
57
+
58
+ rejected_credit = DecisionAgent::Dmn::TreeNode.new(
59
+ id: "rejected_credit",
60
+ label: "Rejected - Poor Credit",
61
+ decision: "Rejected - Poor Credit"
62
+ )
63
+ good_credit.add_child(rejected_credit)
64
+
65
+ # Rejected for age
66
+ rejected_age = DecisionAgent::Dmn::TreeNode.new(
67
+ id: "rejected_age",
68
+ label: "Rejected - Too Young",
69
+ decision: "Rejected - Too Young"
70
+ )
71
+ root.add_child(rejected_age)
72
+
73
+ tree
74
+ end
75
+
76
+ it "evaluates tree with context matching approved path" do
77
+ context = { age: 25, credit_score: 700 }
78
+ result = tree.evaluate(context)
79
+ expect(result).to eq("Approved")
80
+ end
81
+
82
+ it "evaluates tree with poor credit" do
83
+ context = { age: 25, credit_score: 600 }
84
+ result = tree.evaluate(context)
85
+ expect(result).to eq("Rejected - Poor Credit")
86
+ end
87
+
88
+ it "evaluates tree with age too young" do
89
+ context = { age: 16, credit_score: 700 }
90
+ result = tree.evaluate(context)
91
+ expect(result).to eq("Rejected - Too Young")
92
+ end
93
+
94
+ it "returns nil when no path matches" do
95
+ context = {}
96
+ result = tree.evaluate(context)
97
+ expect(result).to be_nil
98
+ end
99
+ end
100
+
101
+ describe "tree serialization" do
102
+ let(:tree_hash) do
103
+ {
104
+ id: "test_tree",
105
+ name: "Test Tree",
106
+ root: {
107
+ id: "root",
108
+ label: "Root",
109
+ condition: nil,
110
+ decision: nil,
111
+ children: [
112
+ {
113
+ id: "node1",
114
+ label: "Node 1",
115
+ condition: "x > 5",
116
+ decision: nil,
117
+ children: [
118
+ {
119
+ id: "leaf1",
120
+ label: "Leaf 1",
121
+ condition: nil,
122
+ decision: "Result A",
123
+ children: []
124
+ }
125
+ ]
126
+ }
127
+ ]
128
+ }
129
+ }
130
+ end
131
+
132
+ it "converts tree to hash" do
133
+ tree = described_class.new(id: "tree1", name: "Tree 1")
134
+ node1 = DecisionAgent::Dmn::TreeNode.new(id: "node1", condition: "x > 5")
135
+ tree.root.add_child(node1)
136
+
137
+ hash = tree.to_h
138
+ expect(hash[:id]).to eq("tree1")
139
+ expect(hash[:name]).to eq("Tree 1")
140
+ expect(hash[:root][:children].length).to eq(1)
141
+ end
142
+
143
+ it "builds tree from hash" do
144
+ tree = described_class.from_hash(tree_hash)
145
+
146
+ expect(tree.id).to eq("test_tree")
147
+ expect(tree.name).to eq("Test Tree")
148
+ expect(tree.root.children.length).to eq(1)
149
+ expect(tree.root.children.first.id).to eq("node1")
150
+ end
151
+ end
152
+
153
+ describe "tree analysis" do
154
+ let(:tree) do
155
+ tree = described_class.new(id: "analysis_tree", name: "Analysis Tree")
156
+
157
+ level1 = DecisionAgent::Dmn::TreeNode.new(id: "level1")
158
+ tree.root.add_child(level1)
159
+
160
+ level2a = DecisionAgent::Dmn::TreeNode.new(id: "level2a")
161
+ level2b = DecisionAgent::Dmn::TreeNode.new(id: "level2b")
162
+ level1.add_child(level2a)
163
+ level1.add_child(level2b)
164
+
165
+ leaf1 = DecisionAgent::Dmn::TreeNode.new(id: "leaf1", decision: "Result 1")
166
+ leaf2 = DecisionAgent::Dmn::TreeNode.new(id: "leaf2", decision: "Result 2")
167
+ level2a.add_child(leaf1)
168
+ level2b.add_child(leaf2)
169
+
170
+ tree
171
+ end
172
+
173
+ it "collects all leaf nodes" do
174
+ leaves = tree.leaf_nodes
175
+ expect(leaves.length).to eq(2)
176
+ expect(leaves.map(&:id)).to include("leaf1", "leaf2")
177
+ end
178
+
179
+ it "calculates tree depth" do
180
+ expect(tree.depth).to eq(3) # root -> level1 -> level2 -> leaf (depth 3)
181
+ end
182
+
183
+ it "collects all paths from root to leaves" do
184
+ paths = tree.paths
185
+ expect(paths.length).to eq(2)
186
+ expect(paths.first.length).to be >= 3
187
+ end
188
+ end
189
+
190
+ describe DecisionAgent::Dmn::TreeNode do
191
+ it "identifies leaf nodes correctly" do
192
+ node = DecisionAgent::Dmn::TreeNode.new(id: "test", decision: "Decision")
193
+ expect(node.leaf?).to be true
194
+ end
195
+
196
+ it "identifies non-leaf nodes correctly" do
197
+ node = DecisionAgent::Dmn::TreeNode.new(id: "test")
198
+ child = DecisionAgent::Dmn::TreeNode.new(id: "child")
199
+ node.add_child(child)
200
+ expect(node.leaf?).to be false
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,18 @@
1
+ require "spec_helper"
2
+ require "decision_agent/dmn/errors"
3
+
4
+ RSpec.describe "FEEL Errors" do
5
+ describe DecisionAgent::Dmn::FeelParseError do
6
+ it "creates error with message only" do
7
+ error = DecisionAgent::Dmn::FeelParseError.new("Parse failed")
8
+ expect(error.message).to eq("Parse failed")
9
+ end
10
+ end
11
+
12
+ describe DecisionAgent::Dmn::FeelEvaluationError do
13
+ it "creates error with message only" do
14
+ error = DecisionAgent::Dmn::FeelEvaluationError.new("Evaluation failed")
15
+ expect(error.message).to eq("Evaluation failed")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,400 @@
1
+ require "spec_helper"
2
+ require "decision_agent/dmn/feel/functions"
3
+
4
+ RSpec.describe DecisionAgent::Dmn::Feel::Functions do
5
+ describe "String Functions" do
6
+ describe "substring" do
7
+ it "extracts substring with start and length" do
8
+ result = described_class.execute("substring", ["hello world", 1, 5])
9
+ expect(result).to eq("hello")
10
+ end
11
+
12
+ it "extracts substring from start to end" do
13
+ result = described_class.execute("substring", ["hello world", 7])
14
+ expect(result).to eq("world")
15
+ end
16
+
17
+ it "handles 1-based indexing" do
18
+ result = described_class.execute("substring", ["abc", 2, 1])
19
+ expect(result).to eq("b")
20
+ end
21
+ end
22
+
23
+ describe "string length" do
24
+ it "returns length of string" do
25
+ result = described_class.execute("string length", ["hello"])
26
+ expect(result).to eq(5)
27
+ end
28
+
29
+ it "returns 0 for empty string" do
30
+ result = described_class.execute("string length", [""])
31
+ expect(result).to eq(0)
32
+ end
33
+ end
34
+
35
+ describe "upper case" do
36
+ it "converts to uppercase" do
37
+ result = described_class.execute("upper case", ["hello"])
38
+ expect(result).to eq("HELLO")
39
+ end
40
+ end
41
+
42
+ describe "lower case" do
43
+ it "converts to lowercase" do
44
+ result = described_class.execute("lower case", ["HELLO"])
45
+ expect(result).to eq("hello")
46
+ end
47
+ end
48
+
49
+ describe "contains" do
50
+ it "returns true when substring is found" do
51
+ result = described_class.execute("contains", ["hello world", "world"])
52
+ expect(result).to be true
53
+ end
54
+
55
+ it "returns false when substring is not found" do
56
+ result = described_class.execute("contains", ["hello world", "xyz"])
57
+ expect(result).to be false
58
+ end
59
+ end
60
+
61
+ describe "starts with" do
62
+ it "returns true when string starts with prefix" do
63
+ result = described_class.execute("starts with", ["hello world", "hello"])
64
+ expect(result).to be true
65
+ end
66
+
67
+ it "returns false when string does not start with prefix" do
68
+ result = described_class.execute("starts with", ["hello world", "world"])
69
+ expect(result).to be false
70
+ end
71
+ end
72
+
73
+ describe "ends with" do
74
+ it "returns true when string ends with suffix" do
75
+ result = described_class.execute("ends with", ["hello world", "world"])
76
+ expect(result).to be true
77
+ end
78
+
79
+ it "returns false when string does not end with suffix" do
80
+ result = described_class.execute("ends with", ["hello world", "hello"])
81
+ expect(result).to be false
82
+ end
83
+ end
84
+
85
+ describe "substring before" do
86
+ it "returns substring before match" do
87
+ result = described_class.execute("substring before", ["hello world", " "])
88
+ expect(result).to eq("hello")
89
+ end
90
+
91
+ it "returns empty string when match not found" do
92
+ result = described_class.execute("substring before", %w[hello x])
93
+ expect(result).to eq("")
94
+ end
95
+ end
96
+
97
+ describe "substring after" do
98
+ it "returns substring after match" do
99
+ result = described_class.execute("substring after", ["hello world", " "])
100
+ expect(result).to eq("world")
101
+ end
102
+
103
+ it "returns empty string when match not found" do
104
+ result = described_class.execute("substring after", %w[hello x])
105
+ expect(result).to eq("")
106
+ end
107
+ end
108
+
109
+ describe "replace" do
110
+ it "replaces all occurrences" do
111
+ result = described_class.execute("replace", ["hello world", "l", "L"])
112
+ expect(result).to eq("heLLo worLd")
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "Numeric Functions" do
118
+ describe "abs" do
119
+ it "returns absolute value of positive number" do
120
+ result = described_class.execute("abs", [5])
121
+ expect(result).to eq(5.0)
122
+ end
123
+
124
+ it "returns absolute value of negative number" do
125
+ result = described_class.execute("abs", [-5])
126
+ expect(result).to eq(5.0)
127
+ end
128
+ end
129
+
130
+ describe "floor" do
131
+ it "rounds down to integer" do
132
+ result = described_class.execute("floor", [3.7])
133
+ expect(result).to eq(3)
134
+ end
135
+
136
+ it "handles negative numbers" do
137
+ result = described_class.execute("floor", [-3.2])
138
+ expect(result).to eq(-4)
139
+ end
140
+ end
141
+
142
+ describe "ceiling" do
143
+ it "rounds up to integer" do
144
+ result = described_class.execute("ceiling", [3.2])
145
+ expect(result).to eq(4)
146
+ end
147
+
148
+ it "handles negative numbers" do
149
+ result = described_class.execute("ceiling", [-3.7])
150
+ expect(result).to eq(-3)
151
+ end
152
+ end
153
+
154
+ describe "round" do
155
+ it "rounds to nearest integer" do
156
+ result = described_class.execute("round", [3.7])
157
+ expect(result).to eq(4)
158
+ end
159
+
160
+ it "rounds to specified precision" do
161
+ result = described_class.execute("round", [3.14159, 2])
162
+ expect(result).to be_within(0.001).of(3.14)
163
+ end
164
+ end
165
+
166
+ describe "sqrt" do
167
+ it "calculates square root" do
168
+ result = described_class.execute("sqrt", [16])
169
+ expect(result).to eq(4.0)
170
+ end
171
+ end
172
+
173
+ describe "modulo" do
174
+ it "calculates remainder" do
175
+ result = described_class.execute("modulo", [10, 3])
176
+ expect(result).to eq(1.0)
177
+ end
178
+ end
179
+
180
+ describe "odd" do
181
+ it "returns true for odd numbers" do
182
+ result = described_class.execute("odd", [5])
183
+ expect(result).to be true
184
+ end
185
+
186
+ it "returns false for even numbers" do
187
+ result = described_class.execute("odd", [4])
188
+ expect(result).to be false
189
+ end
190
+ end
191
+
192
+ describe "even" do
193
+ it "returns true for even numbers" do
194
+ result = described_class.execute("even", [4])
195
+ expect(result).to be true
196
+ end
197
+
198
+ it "returns false for odd numbers" do
199
+ result = described_class.execute("even", [5])
200
+ expect(result).to be false
201
+ end
202
+ end
203
+ end
204
+
205
+ describe "List Functions" do
206
+ describe "count" do
207
+ it "returns length of list" do
208
+ result = described_class.execute("count", [[1, 2, 3, 4, 5]])
209
+ expect(result).to eq(5)
210
+ end
211
+
212
+ it "returns 0 for empty list" do
213
+ result = described_class.execute("count", [[]])
214
+ expect(result).to eq(0)
215
+ end
216
+
217
+ it "returns 0 for non-array" do
218
+ result = described_class.execute("count", [42])
219
+ expect(result).to eq(0)
220
+ end
221
+ end
222
+
223
+ describe "sum" do
224
+ it "calculates sum of list" do
225
+ result = described_class.execute("sum", [[1, 2, 3, 4, 5]])
226
+ expect(result).to eq(15.0)
227
+ end
228
+
229
+ it "returns 0 for empty list" do
230
+ result = described_class.execute("sum", [[]])
231
+ expect(result).to eq(0)
232
+ end
233
+ end
234
+
235
+ describe "mean" do
236
+ it "calculates average of list" do
237
+ result = described_class.execute("mean", [[1, 2, 3, 4, 5]])
238
+ expect(result).to eq(3.0)
239
+ end
240
+
241
+ it "returns 0 for empty list" do
242
+ result = described_class.execute("mean", [[]])
243
+ expect(result).to eq(0)
244
+ end
245
+ end
246
+
247
+ describe "min" do
248
+ it "returns minimum value from list" do
249
+ result = described_class.execute("min", [[5, 2, 8, 1, 9]])
250
+ expect(result).to eq(1.0)
251
+ end
252
+
253
+ it "returns minimum from multiple arguments" do
254
+ result = described_class.execute("min", [5, 2, 8, 1, 9])
255
+ expect(result).to eq(1.0)
256
+ end
257
+
258
+ it "returns nil for empty list" do
259
+ result = described_class.execute("min", [[]])
260
+ expect(result).to be_nil
261
+ end
262
+ end
263
+
264
+ describe "max" do
265
+ it "returns maximum value from list" do
266
+ result = described_class.execute("max", [[5, 2, 8, 1, 9]])
267
+ expect(result).to eq(9.0)
268
+ end
269
+
270
+ it "returns maximum from multiple arguments" do
271
+ result = described_class.execute("max", [5, 2, 8, 1, 9])
272
+ expect(result).to eq(9.0)
273
+ end
274
+ end
275
+
276
+ describe "append" do
277
+ it "appends items to list" do
278
+ result = described_class.execute("append", [[1, 2], 3, 4])
279
+ expect(result).to eq([1, 2, 3, 4])
280
+ end
281
+ end
282
+
283
+ describe "reverse" do
284
+ it "reverses list" do
285
+ result = described_class.execute("reverse", [[1, 2, 3, 4, 5]])
286
+ expect(result).to eq([5, 4, 3, 2, 1])
287
+ end
288
+ end
289
+
290
+ describe "index of" do
291
+ it "returns 1-based index of element" do
292
+ result = described_class.execute("index of", [[10, 20, 30], 20])
293
+ expect(result).to eq(2)
294
+ end
295
+
296
+ it "returns -1 when element not found" do
297
+ result = described_class.execute("index of", [[10, 20, 30], 40])
298
+ expect(result).to eq(-1)
299
+ end
300
+ end
301
+
302
+ describe "distinct values" do
303
+ it "removes duplicates" do
304
+ result = described_class.execute("distinct values", [[1, 2, 2, 3, 3, 3]])
305
+ expect(result).to eq([1, 2, 3])
306
+ end
307
+ end
308
+ end
309
+
310
+ describe "Boolean Functions" do
311
+ describe "not" do
312
+ it "negates true" do
313
+ result = described_class.execute("not", [true])
314
+ expect(result).to be false
315
+ end
316
+
317
+ it "negates false" do
318
+ result = described_class.execute("not", [false])
319
+ expect(result).to be true
320
+ end
321
+ end
322
+
323
+ describe "all" do
324
+ it "returns true when all items are true" do
325
+ result = described_class.execute("all", [[true, true, true]])
326
+ expect(result).to be true
327
+ end
328
+
329
+ it "returns false when any item is false" do
330
+ result = described_class.execute("all", [[true, false, true]])
331
+ expect(result).to be false
332
+ end
333
+ end
334
+
335
+ describe "any" do
336
+ it "returns true when any item is true" do
337
+ result = described_class.execute("any", [[false, true, false]])
338
+ expect(result).to be true
339
+ end
340
+
341
+ it "returns false when all items are false" do
342
+ result = described_class.execute("any", [[false, false, false]])
343
+ expect(result).to be false
344
+ end
345
+ end
346
+ end
347
+
348
+ describe "Date/Time Functions" do
349
+ describe "date" do
350
+ it "parses ISO 8601 date string" do
351
+ result = described_class.execute("date", ["2024-01-15T10:30:00Z"])
352
+ expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Date)
353
+ end
354
+ end
355
+
356
+ describe "time" do
357
+ it "parses ISO 8601 time string" do
358
+ result = described_class.execute("time", ["2024-01-15T10:30:00Z"])
359
+ expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Time)
360
+ end
361
+ end
362
+
363
+ describe "duration" do
364
+ it "parses ISO 8601 duration" do
365
+ result = described_class.execute("duration", ["P1Y2M3DT4H5M6S"])
366
+ expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Duration)
367
+ expect(result.years).to eq(1)
368
+ expect(result.months).to eq(2)
369
+ end
370
+ end
371
+ end
372
+
373
+ describe "Function Registry" do
374
+ it "lists all registered functions" do
375
+ functions = described_class.list
376
+ expect(functions).to include("substring")
377
+ expect(functions).to include("sum")
378
+ expect(functions).to include("abs")
379
+ end
380
+
381
+ it "gets function by name" do
382
+ func = described_class.get("substring")
383
+ expect(func).not_to be_nil
384
+ end
385
+
386
+ it "raises error for unknown function" do
387
+ expect do
388
+ described_class.execute("unknown_func", [])
389
+ end.to raise_error(DecisionAgent::Dmn::FeelFunctionError, /Unknown function/)
390
+ end
391
+ end
392
+
393
+ describe "Argument Validation" do
394
+ it "raises error for wrong argument count" do
395
+ expect do
396
+ described_class.execute("substring", ["hello"])
397
+ end.to raise_error(DecisionAgent::Dmn::FeelFunctionError, /Wrong number of arguments/)
398
+ end
399
+ end
400
+ end