decision_agent 0.3.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
data/spec/dmn/feel/types_spec.rb
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent/dmn/feel/types"
|
|
3
|
-
|
|
4
|
-
RSpec.describe DecisionAgent::Dmn::Feel::Types do
|
|
5
|
-
describe DecisionAgent::Dmn::Feel::Types::Number do
|
|
6
|
-
it "creates from integer" do
|
|
7
|
-
num = DecisionAgent::Dmn::Feel::Types::Number.new(42)
|
|
8
|
-
expect(num.to_ruby).to eq(42)
|
|
9
|
-
expect(num.to_i).to eq(42)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
it "creates from float" do
|
|
13
|
-
num = DecisionAgent::Dmn::Feel::Types::Number.new(3.14)
|
|
14
|
-
expect(num.to_ruby).to eq(3.14)
|
|
15
|
-
expect(num.to_f).to be_within(0.001).of(3.14)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
it "creates from string" do
|
|
19
|
-
num = DecisionAgent::Dmn::Feel::Types::Number.new("42.5")
|
|
20
|
-
expect(num.to_f).to be_within(0.001).of(42.5)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it "supports scale tracking" do
|
|
24
|
-
num = DecisionAgent::Dmn::Feel::Types::Number.new(42, scale: 2)
|
|
25
|
-
expect(num.scale).to eq(2)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it "raises error for invalid type" do
|
|
29
|
-
expect do
|
|
30
|
-
DecisionAgent::Dmn::Feel::Types::Number.new([])
|
|
31
|
-
end.to raise_error(DecisionAgent::Dmn::FeelTypeError)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
describe DecisionAgent::Dmn::Feel::Types::Date do
|
|
36
|
-
it "creates from Time object" do
|
|
37
|
-
time = Time.new(2024, 1, 15)
|
|
38
|
-
date = DecisionAgent::Dmn::Feel::Types::Date.new(time)
|
|
39
|
-
expect(date.to_ruby).to eq(time)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "creates from ISO 8601 string" do
|
|
43
|
-
date = DecisionAgent::Dmn::Feel::Types::Date.new("2024-01-15T10:30:00Z")
|
|
44
|
-
expect(date.to_ruby).to be_a(Time)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it "creates from date string" do
|
|
48
|
-
date = DecisionAgent::Dmn::Feel::Types::Date.new("2024-01-15")
|
|
49
|
-
expect(date.to_ruby).to be_a(Time)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
it "raises error for invalid format" do
|
|
53
|
-
expect do
|
|
54
|
-
DecisionAgent::Dmn::Feel::Types::Date.new("invalid")
|
|
55
|
-
end.to raise_error(DecisionAgent::Dmn::FeelTypeError)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
describe DecisionAgent::Dmn::Feel::Types::Time do
|
|
60
|
-
it "creates from Time object" do
|
|
61
|
-
time = Time.new(2024, 1, 15, 10, 30, 0)
|
|
62
|
-
feel_time = DecisionAgent::Dmn::Feel::Types::Time.new(time)
|
|
63
|
-
expect(feel_time.to_ruby).to eq(time)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it "creates from ISO 8601 string" do
|
|
67
|
-
feel_time = DecisionAgent::Dmn::Feel::Types::Time.new("2024-01-15T10:30:00Z")
|
|
68
|
-
expect(feel_time.to_ruby).to be_a(Time)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
describe DecisionAgent::Dmn::Feel::Types::Duration do
|
|
73
|
-
it "parses ISO 8601 duration with years" do
|
|
74
|
-
duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P1Y")
|
|
75
|
-
expect(duration.years).to eq(1)
|
|
76
|
-
expect(duration.months).to eq(0)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
it "parses ISO 8601 duration with months" do
|
|
80
|
-
duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P3M")
|
|
81
|
-
expect(duration.months).to eq(3)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it "parses ISO 8601 duration with days" do
|
|
85
|
-
duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P10D")
|
|
86
|
-
expect(duration.days).to eq(10)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "parses ISO 8601 duration with time components" do
|
|
90
|
-
duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("PT5H30M15S")
|
|
91
|
-
expect(duration.hours).to eq(5)
|
|
92
|
-
expect(duration.minutes).to eq(30)
|
|
93
|
-
expect(duration.seconds).to eq(15)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
it "parses complete ISO 8601 duration" do
|
|
97
|
-
duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P1Y2M3DT4H5M6S")
|
|
98
|
-
expect(duration.years).to eq(1)
|
|
99
|
-
expect(duration.months).to eq(2)
|
|
100
|
-
expect(duration.days).to eq(3)
|
|
101
|
-
expect(duration.hours).to eq(4)
|
|
102
|
-
expect(duration.minutes).to eq(5)
|
|
103
|
-
expect(duration.seconds).to eq(6)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
it "converts to seconds" do
|
|
107
|
-
duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("PT1H30M")
|
|
108
|
-
expect(duration.to_seconds).to eq(5400) # 90 minutes
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
it "raises error for invalid format" do
|
|
112
|
-
expect do
|
|
113
|
-
DecisionAgent::Dmn::Feel::Types::Duration.parse("invalid")
|
|
114
|
-
end.to raise_error(DecisionAgent::Dmn::FeelTypeError)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
it "raises error for non-P prefix" do
|
|
118
|
-
expect do
|
|
119
|
-
DecisionAgent::Dmn::Feel::Types::Duration.parse("1Y2M")
|
|
120
|
-
end.to raise_error(DecisionAgent::Dmn::FeelTypeError, /must start with 'P'/)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
describe DecisionAgent::Dmn::Feel::Types::List do
|
|
125
|
-
it "wraps array" do
|
|
126
|
-
list = DecisionAgent::Dmn::Feel::Types::List.new([1, 2, 3])
|
|
127
|
-
expect(list.to_ruby).to eq([1, 2, 3])
|
|
128
|
-
expect(list[0]).to eq(1)
|
|
129
|
-
expect(list.length).to eq(3)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
describe DecisionAgent::Dmn::Feel::Types::Context do
|
|
134
|
-
it "wraps hash with symbol keys" do
|
|
135
|
-
ctx = DecisionAgent::Dmn::Feel::Types::Context.new({ "name" => "John", "age" => 30 })
|
|
136
|
-
expect(ctx[:name]).to eq("John")
|
|
137
|
-
expect(ctx[:age]).to eq(30)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
it "converts string keys to symbols" do
|
|
141
|
-
ctx = DecisionAgent::Dmn::Feel::Types::Context.new({ "x" => 10, "y" => 20 })
|
|
142
|
-
expect(ctx.to_ruby).to eq({ x: 10, y: 20 })
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
describe DecisionAgent::Dmn::Feel::Types::Converter do
|
|
147
|
-
it "converts integer to Number" do
|
|
148
|
-
result = DecisionAgent::Dmn::Feel::Types::Converter.to_feel_type(42)
|
|
149
|
-
expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Number)
|
|
150
|
-
expect(result.to_ruby).to eq(42)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
it "converts array to List" do
|
|
154
|
-
result = DecisionAgent::Dmn::Feel::Types::Converter.to_feel_type([1, 2, 3])
|
|
155
|
-
expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::List)
|
|
156
|
-
expect(result.to_ruby).to eq([1, 2, 3])
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
it "converts hash to Context" do
|
|
160
|
-
result = DecisionAgent::Dmn::Feel::Types::Converter.to_feel_type({ x: 10 })
|
|
161
|
-
expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Context)
|
|
162
|
-
expect(result.to_ruby).to eq({ x: 10 })
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
it "converts FEEL types to Ruby" do
|
|
166
|
-
num = DecisionAgent::Dmn::Feel::Types::Number.new(42)
|
|
167
|
-
result = DecisionAgent::Dmn::Feel::Types::Converter.to_ruby(num)
|
|
168
|
-
expect(result).to eq(42)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
it "returns non-FEEL types as-is" do
|
|
172
|
-
result = DecisionAgent::Dmn::Feel::Types::Converter.to_ruby("hello")
|
|
173
|
-
expect(result).to eq("hello")
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent/dmn/feel/parser"
|
|
3
|
-
require "decision_agent/dmn/feel/transformer"
|
|
4
|
-
require "decision_agent/dmn/feel/evaluator"
|
|
5
|
-
|
|
6
|
-
RSpec.describe "FEEL Parser and Evaluator" do
|
|
7
|
-
let(:parser) { DecisionAgent::Dmn::Feel::Parser.new }
|
|
8
|
-
let(:transformer) { DecisionAgent::Dmn::Feel::Transformer.new }
|
|
9
|
-
let(:evaluator) { DecisionAgent::Dmn::Feel::Evaluator.new }
|
|
10
|
-
|
|
11
|
-
describe "Literals" do
|
|
12
|
-
it "parses numbers" do
|
|
13
|
-
result = parser.parse("42")
|
|
14
|
-
ast = transformer.apply(result)
|
|
15
|
-
expect(ast[:type]).to eq(:number)
|
|
16
|
-
expect(ast[:value]).to eq(42)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it "parses negative numbers" do
|
|
20
|
-
result = parser.parse("-42")
|
|
21
|
-
ast = transformer.apply(result)
|
|
22
|
-
expect(ast[:type]).to eq(:number)
|
|
23
|
-
expect(ast[:value]).to eq(-42)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it "parses floats" do
|
|
27
|
-
result = parser.parse("3.14")
|
|
28
|
-
ast = transformer.apply(result)
|
|
29
|
-
expect(ast[:type]).to eq(:number)
|
|
30
|
-
expect(ast[:value]).to eq(3.14)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
it "parses strings" do
|
|
34
|
-
result = parser.parse('"hello world"')
|
|
35
|
-
ast = transformer.apply(result)
|
|
36
|
-
expect(ast[:type]).to eq(:string)
|
|
37
|
-
expect(ast[:value]).to eq("hello world")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it "parses booleans" do
|
|
41
|
-
true_result = parser.parse("true")
|
|
42
|
-
true_ast = transformer.apply(true_result)
|
|
43
|
-
expect(true_ast[:type]).to eq(:boolean)
|
|
44
|
-
expect(true_ast[:value]).to eq(true)
|
|
45
|
-
|
|
46
|
-
false_result = parser.parse("false")
|
|
47
|
-
false_ast = transformer.apply(false_result)
|
|
48
|
-
expect(false_ast[:type]).to eq(:boolean)
|
|
49
|
-
expect(false_ast[:value]).to eq(false)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
it "parses null" do
|
|
53
|
-
result = parser.parse("null")
|
|
54
|
-
ast = transformer.apply(result)
|
|
55
|
-
expect(ast[:type]).to eq(:null)
|
|
56
|
-
expect(ast[:value]).to be_nil
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
describe "Arithmetic Operations" do
|
|
61
|
-
let(:context) { {} }
|
|
62
|
-
|
|
63
|
-
it "evaluates addition" do
|
|
64
|
-
result = evaluator.evaluate("5 + 3", "result", context)
|
|
65
|
-
expect(result).to eq(8)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
it "evaluates subtraction" do
|
|
69
|
-
result = evaluator.evaluate("10 - 4", "result", context)
|
|
70
|
-
expect(result).to eq(6)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it "evaluates multiplication" do
|
|
74
|
-
result = evaluator.evaluate("6 * 7", "result", context)
|
|
75
|
-
expect(result).to eq(42)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "evaluates division" do
|
|
79
|
-
result = evaluator.evaluate("20 / 4", "result", context)
|
|
80
|
-
expect(result).to eq(5.0)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
it "evaluates exponentiation" do
|
|
84
|
-
result = evaluator.evaluate("2 ** 3", "result", context)
|
|
85
|
-
expect(result).to eq(8)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
it "evaluates modulo" do
|
|
89
|
-
result = evaluator.evaluate("10 % 3", "result", context)
|
|
90
|
-
expect(result).to eq(1)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
it "respects operator precedence" do
|
|
94
|
-
result = evaluator.evaluate("2 + 3 * 4", "result", context)
|
|
95
|
-
expect(result).to eq(14)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
it "evaluates parentheses" do
|
|
99
|
-
result = evaluator.evaluate("(2 + 3) * 4", "result", context)
|
|
100
|
-
expect(result).to eq(20)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
describe "Comparison Operations" do
|
|
105
|
-
let(:context) { {} }
|
|
106
|
-
|
|
107
|
-
it "evaluates equality" do
|
|
108
|
-
expect(evaluator.evaluate("5 = 5", "result", context)).to eq(true)
|
|
109
|
-
expect(evaluator.evaluate("5 = 3", "result", context)).to eq(false)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
it "evaluates inequality" do
|
|
113
|
-
expect(evaluator.evaluate("5 != 3", "result", context)).to eq(true)
|
|
114
|
-
expect(evaluator.evaluate("5 != 5", "result", context)).to eq(false)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
it "evaluates less than" do
|
|
118
|
-
expect(evaluator.evaluate("3 < 5", "result", context)).to eq(true)
|
|
119
|
-
expect(evaluator.evaluate("5 < 3", "result", context)).to eq(false)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it "evaluates greater than" do
|
|
123
|
-
expect(evaluator.evaluate("5 > 3", "result", context)).to eq(true)
|
|
124
|
-
expect(evaluator.evaluate("3 > 5", "result", context)).to eq(false)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it "evaluates less than or equal" do
|
|
128
|
-
expect(evaluator.evaluate("3 <= 5", "result", context)).to eq(true)
|
|
129
|
-
expect(evaluator.evaluate("5 <= 5", "result", context)).to eq(true)
|
|
130
|
-
expect(evaluator.evaluate("7 <= 5", "result", context)).to eq(false)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
it "evaluates greater than or equal" do
|
|
134
|
-
expect(evaluator.evaluate("5 >= 3", "result", context)).to eq(true)
|
|
135
|
-
expect(evaluator.evaluate("5 >= 5", "result", context)).to eq(true)
|
|
136
|
-
expect(evaluator.evaluate("3 >= 5", "result", context)).to eq(false)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
describe "Logical Operations" do
|
|
141
|
-
let(:context) { {} }
|
|
142
|
-
|
|
143
|
-
it "evaluates AND" do
|
|
144
|
-
expect(evaluator.evaluate("true and true", "result", context)).to eq(true)
|
|
145
|
-
expect(evaluator.evaluate("true and false", "result", context)).to eq(false)
|
|
146
|
-
expect(evaluator.evaluate("false and false", "result", context)).to eq(false)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "evaluates OR" do
|
|
150
|
-
expect(evaluator.evaluate("true or false", "result", context)).to eq(true)
|
|
151
|
-
expect(evaluator.evaluate("false or true", "result", context)).to eq(true)
|
|
152
|
-
expect(evaluator.evaluate("false or false", "result", context)).to eq(false)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
it "evaluates NOT" do
|
|
156
|
-
expect(evaluator.evaluate("not true", "result", context)).to eq(false)
|
|
157
|
-
expect(evaluator.evaluate("not false", "result", context)).to eq(true)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
it "evaluates complex logical expressions" do
|
|
161
|
-
result = evaluator.evaluate("(5 > 3) and (10 < 20)", "result", context)
|
|
162
|
-
expect(result).to eq(true)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
describe "Field References" do
|
|
167
|
-
it "evaluates field references" do
|
|
168
|
-
context = { age: 25 }
|
|
169
|
-
result = evaluator.evaluate("age", "result", context)
|
|
170
|
-
expect(result).to eq(25)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
it "evaluates field references in comparisons" do
|
|
174
|
-
context = { age: 25 }
|
|
175
|
-
result = evaluator.evaluate("age >= 18", "age", context)
|
|
176
|
-
expect(result).to eq(true)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
it "evaluates field references in arithmetic" do
|
|
180
|
-
context = { price: 100, quantity: 5 }
|
|
181
|
-
result = evaluator.evaluate("price * quantity", "total", context)
|
|
182
|
-
expect(result).to eq(500)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
describe "List Literals" do
|
|
187
|
-
it "parses empty lists" do
|
|
188
|
-
result = parser.parse("[]")
|
|
189
|
-
ast = transformer.apply(result)
|
|
190
|
-
expect(ast[:type]).to eq(:list_literal)
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
it "parses lists with elements" do
|
|
194
|
-
result = parser.parse("[1, 2, 3]")
|
|
195
|
-
ast = transformer.apply(result)
|
|
196
|
-
expect(ast[:type]).to eq(:list_literal)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
it "evaluates list literals" do
|
|
200
|
-
context = {}
|
|
201
|
-
result = evaluator.evaluate("[1, 2, 3]", "list", context)
|
|
202
|
-
expect(result).to eq([1, 2, 3])
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
describe "Context Literals" do
|
|
207
|
-
it "parses empty contexts" do
|
|
208
|
-
result = parser.parse("{}")
|
|
209
|
-
ast = transformer.apply(result)
|
|
210
|
-
expect(ast[:type]).to eq(:context_literal)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
it "parses contexts with entries" do
|
|
214
|
-
result = parser.parse('{ name: "John", age: 30 }')
|
|
215
|
-
ast = transformer.apply(result)
|
|
216
|
-
expect(ast[:type]).to eq(:context_literal)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
it "evaluates context literals" do
|
|
220
|
-
context = {}
|
|
221
|
-
result = evaluator.evaluate("{ a: 1, b: 2 }", "ctx", context)
|
|
222
|
-
expect(result).to eq({ a: 1, b: 2 })
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
describe "Function Calls" do
|
|
227
|
-
let(:context) { {} }
|
|
228
|
-
|
|
229
|
-
it "evaluates string length function" do
|
|
230
|
-
result = evaluator.evaluate('length("hello")', "result", context)
|
|
231
|
-
expect(result).to eq(5)
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
it "evaluates substring function" do
|
|
235
|
-
result = evaluator.evaluate('substring("hello", 2, 3)', "result", context)
|
|
236
|
-
expect(result).to eq("ell")
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
it "evaluates upper case function" do
|
|
240
|
-
result = evaluator.evaluate('upper("hello")', "result", context)
|
|
241
|
-
expect(result).to eq("HELLO")
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
it "evaluates sum function" do
|
|
245
|
-
result = evaluator.evaluate("sum([1, 2, 3, 4])", "result", context)
|
|
246
|
-
expect(result).to eq(10.0)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
it "evaluates mean function" do
|
|
250
|
-
result = evaluator.evaluate("mean([10, 20, 30])", "result", context)
|
|
251
|
-
expect(result).to eq(20.0)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
it "evaluates min function" do
|
|
255
|
-
result = evaluator.evaluate("min([5, 2, 8, 1])", "result", context)
|
|
256
|
-
expect(result).to eq(1.0)
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
it "evaluates max function" do
|
|
260
|
-
result = evaluator.evaluate("max([5, 2, 8, 1])", "result", context)
|
|
261
|
-
expect(result).to eq(8.0)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
describe "If-Then-Else Conditionals" do
|
|
266
|
-
let(:context) { {} }
|
|
267
|
-
|
|
268
|
-
it "evaluates true condition" do
|
|
269
|
-
result = evaluator.evaluate('if 5 > 3 then "big" else "small"', "result", context)
|
|
270
|
-
expect(result).to eq("big")
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
it "evaluates false condition" do
|
|
274
|
-
result = evaluator.evaluate('if 3 > 5 then "big" else "small"', "result", context)
|
|
275
|
-
expect(result).to eq("small")
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
it "evaluates with field references" do
|
|
279
|
-
context = { age: 25 }
|
|
280
|
-
result = evaluator.evaluate('if age >= 18 then "adult" else "minor"', "status", context)
|
|
281
|
-
expect(result).to eq("adult")
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
it "evaluates nested conditionals" do
|
|
285
|
-
context = { score: 85 }
|
|
286
|
-
result = evaluator.evaluate(
|
|
287
|
-
'if score >= 90 then "A" else if score >= 80 then "B" else "C"',
|
|
288
|
-
"grade",
|
|
289
|
-
context
|
|
290
|
-
)
|
|
291
|
-
expect(result).to eq("B")
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
describe "Quantified Expressions" do
|
|
296
|
-
it "evaluates 'some' expression - true case" do
|
|
297
|
-
context = {}
|
|
298
|
-
result = evaluator.evaluate("some x in [1, 5, 10] satisfies x > 8", "result", context)
|
|
299
|
-
expect(result).to eq(true)
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
it "evaluates 'some' expression - false case" do
|
|
303
|
-
context = {}
|
|
304
|
-
result = evaluator.evaluate("some x in [1, 2, 3] satisfies x > 10", "result", context)
|
|
305
|
-
expect(result).to eq(false)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
it "evaluates 'every' expression - true case" do
|
|
309
|
-
context = {}
|
|
310
|
-
result = evaluator.evaluate("every x in [5, 10, 15] satisfies x > 0", "result", context)
|
|
311
|
-
expect(result).to eq(true)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
it "evaluates 'every' expression - false case" do
|
|
315
|
-
context = {}
|
|
316
|
-
result = evaluator.evaluate("every x in [1, 5, 10] satisfies x > 5", "result", context)
|
|
317
|
-
expect(result).to eq(false)
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
describe "For Expressions" do
|
|
322
|
-
it "evaluates for expression with arithmetic" do
|
|
323
|
-
context = {}
|
|
324
|
-
result = evaluator.evaluate("for x in [1, 2, 3] return x * 2", "result", context)
|
|
325
|
-
expect(result).to eq([2, 4, 6])
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
it "evaluates for expression with addition" do
|
|
329
|
-
context = {}
|
|
330
|
-
result = evaluator.evaluate("for x in [10, 20, 30] return x + 5", "result", context)
|
|
331
|
-
expect(result).to eq([15, 25, 35])
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
describe "Between Expressions" do
|
|
336
|
-
it "evaluates between - true case" do
|
|
337
|
-
context = {}
|
|
338
|
-
result = evaluator.evaluate("5 between 1 and 10", "result", context)
|
|
339
|
-
expect(result).to eq(true)
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
it "evaluates between - false case" do
|
|
343
|
-
context = {}
|
|
344
|
-
result = evaluator.evaluate("15 between 1 and 10", "result", context)
|
|
345
|
-
expect(result).to eq(false)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
it "evaluates between with field reference" do
|
|
349
|
-
context = { age: 25 }
|
|
350
|
-
result = evaluator.evaluate("age between 18 and 65", "working_age", context)
|
|
351
|
-
expect(result).to eq(true)
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
describe "Range Literals" do
|
|
356
|
-
it "parses inclusive range" do
|
|
357
|
-
result = parser.parse("[1..10]")
|
|
358
|
-
ast = transformer.apply(result)
|
|
359
|
-
expect(ast[:type]).to eq(:range)
|
|
360
|
-
expect(ast[:start_inclusive]).to eq(true)
|
|
361
|
-
expect(ast[:end_inclusive]).to eq(true)
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
it "parses exclusive start range" do
|
|
365
|
-
result = parser.parse("(1..10]")
|
|
366
|
-
ast = transformer.apply(result)
|
|
367
|
-
expect(ast[:type]).to eq(:range)
|
|
368
|
-
expect(ast[:start_inclusive]).to eq(false)
|
|
369
|
-
expect(ast[:end_inclusive]).to eq(true)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
it "parses exclusive end range" do
|
|
373
|
-
result = parser.parse("[1..10)")
|
|
374
|
-
ast = transformer.apply(result)
|
|
375
|
-
expect(ast[:type]).to eq(:range)
|
|
376
|
-
expect(ast[:start_inclusive]).to eq(true)
|
|
377
|
-
expect(ast[:end_inclusive]).to eq(false)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
it "parses fully exclusive range" do
|
|
381
|
-
result = parser.parse("(1..10)")
|
|
382
|
-
ast = transformer.apply(result)
|
|
383
|
-
expect(ast[:type]).to eq(:range)
|
|
384
|
-
expect(ast[:start_inclusive]).to eq(false)
|
|
385
|
-
expect(ast[:end_inclusive]).to eq(false)
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
describe "In Expressions" do
|
|
390
|
-
it "evaluates in with list - true case" do
|
|
391
|
-
context = {}
|
|
392
|
-
result = evaluator.evaluate("5 in [1, 3, 5, 7]", "result", context)
|
|
393
|
-
expect(result).to eq(true)
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
it "evaluates in with list - false case" do
|
|
397
|
-
context = {}
|
|
398
|
-
result = evaluator.evaluate("4 in [1, 3, 5, 7]", "result", context)
|
|
399
|
-
expect(result).to eq(false)
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
describe "Instance Of Expressions" do
|
|
404
|
-
it "checks number type" do
|
|
405
|
-
context = {}
|
|
406
|
-
expect(evaluator.evaluate("42 instance of number", "result", context)).to eq(true)
|
|
407
|
-
expect(evaluator.evaluate('"hello" instance of number', "result", context)).to eq(false)
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
it "checks string type" do
|
|
411
|
-
context = {}
|
|
412
|
-
expect(evaluator.evaluate('"hello" instance of string', "result", context)).to eq(true)
|
|
413
|
-
expect(evaluator.evaluate("42 instance of string", "result", context)).to eq(false)
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
it "checks boolean type" do
|
|
417
|
-
context = {}
|
|
418
|
-
expect(evaluator.evaluate("true instance of boolean", "result", context)).to eq(true)
|
|
419
|
-
expect(evaluator.evaluate("42 instance of boolean", "result", context)).to eq(false)
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
it "checks list type" do
|
|
423
|
-
context = {}
|
|
424
|
-
expect(evaluator.evaluate("[1, 2, 3] instance of list", "result", context)).to eq(true)
|
|
425
|
-
expect(evaluator.evaluate("42 instance of list", "result", context)).to eq(false)
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
it "checks context type" do
|
|
429
|
-
context = {}
|
|
430
|
-
expect(evaluator.evaluate("{a: 1} instance of context", "result", context)).to eq(true)
|
|
431
|
-
expect(evaluator.evaluate("42 instance of context", "result", context)).to eq(false)
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
describe "Complex Expressions" do
|
|
436
|
-
it "evaluates complex business rule" do
|
|
437
|
-
context = {
|
|
438
|
-
age: 25,
|
|
439
|
-
income: 50_000,
|
|
440
|
-
credit_score: 720
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
expr = "if age >= 18 and income >= 30000 and credit_score >= 650 then \"approved\" else \"denied\""
|
|
444
|
-
result = evaluator.evaluate(expr, "loan_status", context)
|
|
445
|
-
expect(result).to eq("approved")
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
it "evaluates nested arithmetic with comparisons" do
|
|
449
|
-
context = {
|
|
450
|
-
price: 100,
|
|
451
|
-
quantity: 5,
|
|
452
|
-
discount: 10
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
expr = "(price * quantity) - discount > 400"
|
|
456
|
-
result = evaluator.evaluate(expr, "qualifies", context)
|
|
457
|
-
expect(result).to eq(true)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
it "combines lists and functions" do
|
|
461
|
-
context = {}
|
|
462
|
-
expr = "sum([1, 2, 3]) + max([4, 5, 6])"
|
|
463
|
-
result = evaluator.evaluate(expr, "result", context)
|
|
464
|
-
expect(result).to eq(12.0)
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
it "evaluates filter-like expression with quantifier" do
|
|
468
|
-
context = {}
|
|
469
|
-
expr = "some x in [10, 20, 30] satisfies x > 15"
|
|
470
|
-
result = evaluator.evaluate(expr, "has_large", context)
|
|
471
|
-
expect(result).to eq(true)
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
describe "Error Handling" do
|
|
476
|
-
it "raises error for invalid syntax" do
|
|
477
|
-
expect do
|
|
478
|
-
parser.parse("5 +")
|
|
479
|
-
end.to raise_error(DecisionAgent::Dmn::FeelParseError)
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
it "falls back gracefully for unsupported expressions" do
|
|
483
|
-
context = {}
|
|
484
|
-
# Should fall back to literal equality
|
|
485
|
-
result = evaluator.evaluate("unknown_syntax", "field", context)
|
|
486
|
-
expect(result).to be_a(Hash) # Returns condition structure
|
|
487
|
-
end
|
|
488
|
-
end
|
|
489
|
-
end
|