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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,774 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe DecisionAgent::Dsl::ConditionEvaluator do
4
- let(:context) { DecisionAgent::Context.new({ status: "active", age: 30, score: 85 }) }
5
-
6
- describe ".evaluate" do
7
- context "with invalid input" do
8
- it "returns false for non-hash condition" do
9
- result = described_class.evaluate("not a hash", context)
10
- expect(result).to be false
11
- end
12
-
13
- it "returns false for nil condition" do
14
- result = described_class.evaluate(nil, context)
15
- expect(result).to be false
16
- end
17
-
18
- it "returns false for condition without field, all, or any" do
19
- result = described_class.evaluate({ invalid: "key" }, context)
20
- expect(result).to be false
21
- end
22
- end
23
-
24
- context "with 'all' condition" do
25
- it "evaluates all conditions" do
26
- condition = {
27
- "all" => [
28
- { "field" => "status", "op" => "eq", "value" => "active" },
29
- { "field" => "age", "op" => "gt", "value" => 18 }
30
- ]
31
- }
32
- result = described_class.evaluate(condition, context)
33
- expect(result).to be true
34
- end
35
-
36
- it "returns false if any condition fails" do
37
- condition = {
38
- "all" => [
39
- { "field" => "status", "op" => "eq", "value" => "active" },
40
- { "field" => "age", "op" => "gt", "value" => 100 }
41
- ]
42
- }
43
- result = described_class.evaluate(condition, context)
44
- expect(result).to be false
45
- end
46
- end
47
-
48
- context "with 'any' condition" do
49
- it "evaluates any condition" do
50
- condition = {
51
- "any" => [
52
- { "field" => "status", "op" => "eq", "value" => "inactive" },
53
- { "field" => "age", "op" => "gt", "value" => 18 }
54
- ]
55
- }
56
- result = described_class.evaluate(condition, context)
57
- expect(result).to be true
58
- end
59
-
60
- it "returns false if all conditions fail" do
61
- condition = {
62
- "any" => [
63
- { "field" => "status", "op" => "eq", "value" => "inactive" },
64
- { "field" => "age", "op" => "gt", "value" => 100 }
65
- ]
66
- }
67
- result = described_class.evaluate(condition, context)
68
- expect(result).to be false
69
- end
70
- end
71
-
72
- context "with field condition" do
73
- it "evaluates field condition" do
74
- condition = { "field" => "status", "op" => "eq", "value" => "active" }
75
- result = described_class.evaluate(condition, context)
76
- expect(result).to be true
77
- end
78
- end
79
- end
80
-
81
- describe ".evaluate_all" do
82
- it "returns true for empty array" do
83
- result = described_class.evaluate_all([], context)
84
- expect(result).to be true
85
- end
86
-
87
- it "returns false for non-array input" do
88
- result = described_class.evaluate_all("not an array", context)
89
- expect(result).to be false
90
- end
91
-
92
- it "returns true when all conditions are true" do
93
- conditions = [
94
- { "field" => "status", "op" => "eq", "value" => "active" },
95
- { "field" => "age", "op" => "gt", "value" => 18 }
96
- ]
97
- result = described_class.evaluate_all(conditions, context)
98
- expect(result).to be true
99
- end
100
-
101
- it "returns false when any condition is false" do
102
- conditions = [
103
- { "field" => "status", "op" => "eq", "value" => "active" },
104
- { "field" => "age", "op" => "gt", "value" => 100 }
105
- ]
106
- result = described_class.evaluate_all(conditions, context)
107
- expect(result).to be false
108
- end
109
- end
110
-
111
- describe ".evaluate_any" do
112
- it "returns false for empty array" do
113
- result = described_class.evaluate_any([], context)
114
- expect(result).to be false
115
- end
116
-
117
- it "returns false for non-array input" do
118
- result = described_class.evaluate_any("not an array", context)
119
- expect(result).to be false
120
- end
121
-
122
- it "returns true when at least one condition is true" do
123
- conditions = [
124
- { "field" => "status", "op" => "eq", "value" => "inactive" },
125
- { "field" => "age", "op" => "gt", "value" => 18 }
126
- ]
127
- result = described_class.evaluate_any(conditions, context)
128
- expect(result).to be true
129
- end
130
-
131
- it "returns false when all conditions are false" do
132
- conditions = [
133
- { "field" => "status", "op" => "eq", "value" => "inactive" },
134
- { "field" => "age", "op" => "gt", "value" => 100 }
135
- ]
136
- result = described_class.evaluate_any(conditions, context)
137
- expect(result).to be false
138
- end
139
- end
140
-
141
- describe ".evaluate_field_condition" do
142
- describe "equality operators" do
143
- it "handles eq operator" do
144
- condition = { "field" => "status", "op" => "eq", "value" => "active" }
145
- result = described_class.evaluate_field_condition(condition, context)
146
- expect(result).to be true
147
- end
148
-
149
- it "handles neq operator" do
150
- condition = { "field" => "status", "op" => "neq", "value" => "inactive" }
151
- result = described_class.evaluate_field_condition(condition, context)
152
- expect(result).to be true
153
- end
154
- end
155
-
156
- describe "comparison operators" do
157
- it "handles gt operator" do
158
- condition = { "field" => "age", "op" => "gt", "value" => 18 }
159
- result = described_class.evaluate_field_condition(condition, context)
160
- expect(result).to be true
161
- end
162
-
163
- it "handles gte operator" do
164
- condition = { "field" => "age", "op" => "gte", "value" => 30 }
165
- result = described_class.evaluate_field_condition(condition, context)
166
- expect(result).to be true
167
- end
168
-
169
- it "handles lt operator" do
170
- condition = { "field" => "age", "op" => "lt", "value" => 40 }
171
- result = described_class.evaluate_field_condition(condition, context)
172
- expect(result).to be true
173
- end
174
-
175
- it "handles lte operator" do
176
- condition = { "field" => "age", "op" => "lte", "value" => 30 }
177
- result = described_class.evaluate_field_condition(condition, context)
178
- expect(result).to be true
179
- end
180
-
181
- it "returns false for incompatible types in comparison" do
182
- condition = { "field" => "status", "op" => "gt", "value" => 10 }
183
- result = described_class.evaluate_field_condition(condition, context)
184
- expect(result).to be false
185
- end
186
- end
187
-
188
- describe "membership operators" do
189
- it "handles in operator" do
190
- condition = { "field" => "status", "op" => "in", "value" => %w[active inactive] }
191
- result = described_class.evaluate_field_condition(condition, context)
192
- expect(result).to be true
193
- end
194
-
195
- it "handles in operator with non-array value" do
196
- condition = { "field" => "status", "op" => "in", "value" => "active" }
197
- result = described_class.evaluate_field_condition(condition, context)
198
- expect(result).to be true
199
- end
200
- end
201
-
202
- describe "presence operators" do
203
- it "handles present operator with non-empty value" do
204
- condition = { "field" => "status", "op" => "present" }
205
- result = described_class.evaluate_field_condition(condition, context)
206
- expect(result).to be true
207
- end
208
-
209
- it "handles present operator with nil" do
210
- ctx = DecisionAgent::Context.new({ status: nil })
211
- condition = { "field" => "status", "op" => "present" }
212
- result = described_class.evaluate_field_condition(condition, ctx)
213
- expect(result).to be false
214
- end
215
-
216
- it "handles present operator with empty string" do
217
- ctx = DecisionAgent::Context.new({ status: "" })
218
- condition = { "field" => "status", "op" => "present" }
219
- result = described_class.evaluate_field_condition(condition, ctx)
220
- expect(result).to be false
221
- end
222
-
223
- it "handles present operator with empty array" do
224
- ctx = DecisionAgent::Context.new({ items: [] })
225
- condition = { "field" => "items", "op" => "present" }
226
- result = described_class.evaluate_field_condition(condition, ctx)
227
- expect(result).to be false
228
- end
229
-
230
- it "handles present operator with zero" do
231
- ctx = DecisionAgent::Context.new({ count: 0 })
232
- condition = { "field" => "count", "op" => "present" }
233
- result = described_class.evaluate_field_condition(condition, ctx)
234
- expect(result).to be true
235
- end
236
-
237
- it "handles present operator with false boolean" do
238
- ctx = DecisionAgent::Context.new({ active: false })
239
- condition = { "field" => "active", "op" => "present" }
240
- result = described_class.evaluate_field_condition(condition, ctx)
241
- expect(result).to be true
242
- end
243
-
244
- it "handles blank operator with nil" do
245
- ctx = DecisionAgent::Context.new({ status: nil })
246
- condition = { "field" => "status", "op" => "blank" }
247
- result = described_class.evaluate_field_condition(condition, ctx)
248
- expect(result).to be true
249
- end
250
-
251
- it "handles blank operator with empty string" do
252
- ctx = DecisionAgent::Context.new({ status: "" })
253
- condition = { "field" => "status", "op" => "blank" }
254
- result = described_class.evaluate_field_condition(condition, ctx)
255
- expect(result).to be true
256
- end
257
-
258
- it "handles blank operator with zero" do
259
- ctx = DecisionAgent::Context.new({ count: 0 })
260
- condition = { "field" => "count", "op" => "blank" }
261
- result = described_class.evaluate_field_condition(condition, ctx)
262
- expect(result).to be false
263
- end
264
-
265
- it "handles blank operator with false boolean" do
266
- ctx = DecisionAgent::Context.new({ active: false })
267
- condition = { "field" => "active", "op" => "blank" }
268
- result = described_class.evaluate_field_condition(condition, ctx)
269
- expect(result).to be false
270
- end
271
- end
272
-
273
- describe "string operators" do
274
- it "handles contains operator" do
275
- ctx = DecisionAgent::Context.new({ message: "Hello world" })
276
- condition = { "field" => "message", "op" => "contains", "value" => "world" }
277
- result = described_class.evaluate_field_condition(condition, ctx)
278
- expect(result).to be true
279
- end
280
-
281
- it "handles starts_with operator" do
282
- ctx = DecisionAgent::Context.new({ code: "ERR_404" })
283
- condition = { "field" => "code", "op" => "starts_with", "value" => "ERR" }
284
- result = described_class.evaluate_field_condition(condition, ctx)
285
- expect(result).to be true
286
- end
287
-
288
- it "handles ends_with operator" do
289
- ctx = DecisionAgent::Context.new({ filename: "document.pdf" })
290
- condition = { "field" => "filename", "op" => "ends_with", "value" => ".pdf" }
291
- result = described_class.evaluate_field_condition(condition, ctx)
292
- expect(result).to be true
293
- end
294
-
295
- it "handles matches operator with string regex" do
296
- ctx = DecisionAgent::Context.new({ email: "user@example.com" })
297
- condition = { "field" => "email", "op" => "matches", "value" => "^[a-z]+@[a-z]+\\.[a-z]+$" }
298
- result = described_class.evaluate_field_condition(condition, ctx)
299
- expect(result).to be true
300
- end
301
-
302
- it "handles matches operator with Regexp object" do
303
- ctx = DecisionAgent::Context.new({ email: "user@example.com" })
304
- condition = { "field" => "email", "op" => "matches", "value" => /^[a-z]+@[a-z]+\.[a-z]+$/ }
305
- result = described_class.evaluate_field_condition(condition, ctx)
306
- expect(result).to be true
307
- end
308
-
309
- it "returns false for matches with non-string value" do
310
- condition = { "field" => "age", "op" => "matches", "value" => "\\d+" }
311
- result = described_class.evaluate_field_condition(condition, context)
312
- expect(result).to be false
313
- end
314
-
315
- it "handles invalid regex gracefully" do
316
- ctx = DecisionAgent::Context.new({ text: "test" })
317
- condition = { "field" => "text", "op" => "matches", "value" => "[invalid(" }
318
- result = described_class.evaluate_field_condition(condition, ctx)
319
- expect(result).to be false
320
- end
321
-
322
- it "returns false for string operators with non-string values" do
323
- condition = { "field" => "age", "op" => "contains", "value" => "30" }
324
- result = described_class.evaluate_field_condition(condition, context)
325
- expect(result).to be false
326
- end
327
- end
328
-
329
- describe "numeric operators" do
330
- it "handles between operator with array" do
331
- condition = { "field" => "age", "op" => "between", "value" => [18, 65] }
332
- result = described_class.evaluate_field_condition(condition, context)
333
- expect(result).to be true
334
- end
335
-
336
- it "handles between operator with hash" do
337
- condition = { "field" => "age", "op" => "between", "value" => { "min" => 18, "max" => 65 } }
338
- result = described_class.evaluate_field_condition(condition, context)
339
- expect(result).to be true
340
- end
341
-
342
- it "returns false for between with non-numeric value" do
343
- condition = { "field" => "status", "op" => "between", "value" => [1, 10] }
344
- result = described_class.evaluate_field_condition(condition, context)
345
- expect(result).to be false
346
- end
347
-
348
- it "handles modulo operator with array" do
349
- condition = { "field" => "age", "op" => "modulo", "value" => [2, 0] }
350
- result = described_class.evaluate_field_condition(condition, context)
351
- expect(result).to be true
352
- end
353
-
354
- it "handles modulo operator with hash" do
355
- condition = { "field" => "age", "op" => "modulo", "value" => { "divisor" => 2, "remainder" => 0 } }
356
- result = described_class.evaluate_field_condition(condition, context)
357
- expect(result).to be true
358
- end
359
-
360
- it "returns false for modulo with non-numeric value" do
361
- condition = { "field" => "status", "op" => "modulo", "value" => [2, 0] }
362
- result = described_class.evaluate_field_condition(condition, context)
363
- expect(result).to be false
364
- end
365
- end
366
-
367
- describe "date/time operators" do
368
- it "handles before_date operator" do
369
- ctx = DecisionAgent::Context.new({ expires_at: "2025-06-01" })
370
- condition = { "field" => "expires_at", "op" => "before_date", "value" => "2025-12-31" }
371
- result = described_class.evaluate_field_condition(condition, ctx)
372
- expect(result).to be true
373
- end
374
-
375
- it "handles after_date operator" do
376
- ctx = DecisionAgent::Context.new({ created_at: "2025-06-01" })
377
- condition = { "field" => "created_at", "op" => "after_date", "value" => "2024-01-01" }
378
- result = described_class.evaluate_field_condition(condition, ctx)
379
- expect(result).to be true
380
- end
381
-
382
- it "handles within_days operator" do
383
- future_date = (Time.now + (3 * 24 * 60 * 60)).strftime("%Y-%m-%d")
384
- ctx = DecisionAgent::Context.new({ event_date: future_date })
385
- condition = { "field" => "event_date", "op" => "within_days", "value" => 7 }
386
- result = described_class.evaluate_field_condition(condition, ctx)
387
- expect(result).to be true
388
- end
389
-
390
- it "handles day_of_week operator with string" do
391
- monday_date = Time.now
392
- monday_date += 24 * 60 * 60 until monday_date.wday == 1
393
- ctx = DecisionAgent::Context.new({ appointment: monday_date.strftime("%Y-%m-%d") })
394
- condition = { "field" => "appointment", "op" => "day_of_week", "value" => "monday" }
395
- result = described_class.evaluate_field_condition(condition, ctx)
396
- expect(result).to be true
397
- end
398
-
399
- it "handles day_of_week operator with numeric" do
400
- monday_date = Time.now
401
- monday_date += 24 * 60 * 60 until monday_date.wday == 1
402
- ctx = DecisionAgent::Context.new({ appointment: monday_date.strftime("%Y-%m-%d") })
403
- condition = { "field" => "appointment", "op" => "day_of_week", "value" => 1 }
404
- result = described_class.evaluate_field_condition(condition, ctx)
405
- expect(result).to be true
406
- end
407
- end
408
-
409
- describe "collection operators" do
410
- it "handles contains_all operator" do
411
- ctx = DecisionAgent::Context.new({ permissions: %w[read write execute] })
412
- condition = { "field" => "permissions", "op" => "contains_all", "value" => %w[read write] }
413
- result = described_class.evaluate_field_condition(condition, ctx)
414
- expect(result).to be true
415
- end
416
-
417
- it "handles contains_any operator" do
418
- ctx = DecisionAgent::Context.new({ tags: %w[normal urgent] })
419
- condition = { "field" => "tags", "op" => "contains_any", "value" => %w[urgent critical] }
420
- result = described_class.evaluate_field_condition(condition, ctx)
421
- expect(result).to be true
422
- end
423
-
424
- it "handles intersects operator" do
425
- ctx = DecisionAgent::Context.new({ user_roles: %w[user moderator] })
426
- condition = { "field" => "user_roles", "op" => "intersects", "value" => %w[admin moderator] }
427
- result = described_class.evaluate_field_condition(condition, ctx)
428
- expect(result).to be true
429
- end
430
-
431
- it "handles subset_of operator" do
432
- ctx = DecisionAgent::Context.new({ selected_items: %w[a c] })
433
- condition = { "field" => "selected_items", "op" => "subset_of", "value" => %w[a b c d] }
434
- result = described_class.evaluate_field_condition(condition, ctx)
435
- expect(result).to be true
436
- end
437
-
438
- it "returns false for collection operators with non-array values" do
439
- condition = { "field" => "status", "op" => "contains_all", "value" => %w[read write] }
440
- result = described_class.evaluate_field_condition(condition, context)
441
- expect(result).to be false
442
- end
443
- end
444
-
445
- describe "geospatial operators" do
446
- it "handles within_radius operator" do
447
- ctx = DecisionAgent::Context.new({ location: { lat: 40.7200, lon: -74.0000 } })
448
- condition = {
449
- "field" => "location",
450
- "op" => "within_radius",
451
- "value" => { "center" => { "lat" => 40.7128, "lon" => -74.0060 }, "radius" => 10 }
452
- }
453
- result = described_class.evaluate_field_condition(condition, ctx)
454
- expect(result).to be true
455
- end
456
-
457
- it "handles in_polygon operator" do
458
- polygon = [
459
- { "lat" => -1, "lon" => -1 },
460
- { "lat" => 1, "lon" => -1 },
461
- { "lat" => 1, "lon" => 1 },
462
- { "lat" => -1, "lon" => 1 }
463
- ]
464
- ctx = DecisionAgent::Context.new({ location: { lat: 0, lon: 0 } })
465
- condition = { "field" => "location", "op" => "in_polygon", "value" => polygon }
466
- result = described_class.evaluate_field_condition(condition, ctx)
467
- expect(result).to be true
468
- end
469
-
470
- it "returns false for in_polygon with less than 3 vertices" do
471
- polygon = [
472
- { "lat" => -1, "lon" => -1 },
473
- { "lat" => 1, "lon" => -1 }
474
- ]
475
- ctx = DecisionAgent::Context.new({ location: { lat: 0, lon: 0 } })
476
- condition = { "field" => "location", "op" => "in_polygon", "value" => polygon }
477
- result = described_class.evaluate_field_condition(condition, ctx)
478
- expect(result).to be false
479
- end
480
- end
481
-
482
- it "returns false for unknown operator" do
483
- condition = { "field" => "status", "op" => "unknown_op", "value" => "active" }
484
- result = described_class.evaluate_field_condition(condition, context)
485
- expect(result).to be false
486
- end
487
- end
488
-
489
- describe ".get_nested_value" do
490
- it "retrieves simple value" do
491
- hash = { status: "active" }
492
- result = described_class.get_nested_value(hash, "status")
493
- expect(result).to eq("active")
494
- end
495
-
496
- it "retrieves nested value with dot notation" do
497
- hash = { user: { role: "admin" } }
498
- result = described_class.get_nested_value(hash, "user.role")
499
- expect(result).to eq("admin")
500
- end
501
-
502
- it "retrieves deeply nested value" do
503
- hash = { user: { profile: { name: "John" } } }
504
- result = described_class.get_nested_value(hash, "user.profile.name")
505
- expect(result).to eq("John")
506
- end
507
-
508
- it "returns nil for missing key" do
509
- hash = { user: { role: "admin" } }
510
- result = described_class.get_nested_value(hash, "user.missing")
511
- expect(result).to be_nil
512
- end
513
-
514
- it "returns nil when intermediate value is nil" do
515
- hash = { user: nil }
516
- result = described_class.get_nested_value(hash, "user.role")
517
- expect(result).to be_nil
518
- end
519
-
520
- it "handles symbol keys" do
521
- hash = { user: { role: "admin" } }
522
- result = described_class.get_nested_value(hash, "user.role")
523
- expect(result).to eq("admin")
524
- end
525
-
526
- it "returns nil for non-hash intermediate value" do
527
- hash = { user: "not a hash" }
528
- result = described_class.get_nested_value(hash, "user.role")
529
- expect(result).to be_nil
530
- end
531
- end
532
-
533
- describe ".comparable?" do
534
- it "returns true for same numeric types" do
535
- result = described_class.comparable?(10, 20)
536
- expect(result).to be true
537
- end
538
-
539
- it "returns true for same string types" do
540
- result = described_class.comparable?("a", "b")
541
- expect(result).to be true
542
- end
543
-
544
- it "returns false for different numeric types" do
545
- result = described_class.comparable?(10, 20.0)
546
- expect(result).to be false
547
- end
548
-
549
- it "returns false for non-comparable types" do
550
- result = described_class.comparable?({}, [])
551
- expect(result).to be false
552
- end
553
- end
554
-
555
- describe ".parse_range" do
556
- it "parses array format" do
557
- result = described_class.parse_range([10, 20])
558
- expect(result).to eq({ min: 10, max: 20 })
559
- end
560
-
561
- it "parses hash format with string keys" do
562
- result = described_class.parse_range({ "min" => 10, "max" => 20 })
563
- expect(result).to eq({ min: 10, max: 20 })
564
- end
565
-
566
- it "parses hash format with symbol keys" do
567
- result = described_class.parse_range({ min: 10, max: 20 })
568
- expect(result).to eq({ min: 10, max: 20 })
569
- end
570
-
571
- it "returns nil for invalid array" do
572
- result = described_class.parse_range([10])
573
- expect(result).to be_nil
574
- end
575
-
576
- it "returns nil for invalid hash" do
577
- result = described_class.parse_range({ min: 10 })
578
- expect(result).to be_nil
579
- end
580
- end
581
-
582
- describe ".parse_modulo_params" do
583
- it "parses array format" do
584
- result = described_class.parse_modulo_params([2, 0])
585
- expect(result).to eq({ divisor: 2, remainder: 0 })
586
- end
587
-
588
- it "parses hash format with string keys" do
589
- result = described_class.parse_modulo_params({ "divisor" => 2, "remainder" => 0 })
590
- expect(result).to eq({ divisor: 2, remainder: 0 })
591
- end
592
-
593
- it "parses hash format with symbol keys" do
594
- result = described_class.parse_modulo_params({ divisor: 2, remainder: 0 })
595
- expect(result).to eq({ divisor: 2, remainder: 0 })
596
- end
597
-
598
- it "returns nil for invalid array" do
599
- result = described_class.parse_modulo_params([2])
600
- expect(result).to be_nil
601
- end
602
-
603
- it "returns nil for invalid hash" do
604
- result = described_class.parse_modulo_params({ divisor: 2 })
605
- expect(result).to be_nil
606
- end
607
- end
608
-
609
- describe ".parse_date" do
610
- it "parses Time object" do
611
- time = Time.now
612
- result = described_class.parse_date(time)
613
- expect(result).to eq(time)
614
- end
615
-
616
- it "parses Date object" do
617
- date = Date.today
618
- result = described_class.parse_date(date)
619
- expect(result).to eq(date)
620
- end
621
-
622
- it "parses DateTime object" do
623
- datetime = DateTime.now
624
- result = described_class.parse_date(datetime)
625
- expect(result).to eq(datetime)
626
- end
627
-
628
- it "parses string date" do
629
- result = described_class.parse_date("2025-01-01")
630
- expect(result).to be_a(Time)
631
- end
632
-
633
- it "returns nil for invalid string" do
634
- result = described_class.parse_date("invalid")
635
- expect(result).to be_nil
636
- end
637
- end
638
-
639
- describe ".compare_dates" do
640
- it "compares dates with < operator" do
641
- result = described_class.compare_dates("2025-01-01", "2025-12-31", :<)
642
- expect(result).to be true
643
- end
644
-
645
- it "compares dates with > operator" do
646
- result = described_class.compare_dates("2025-12-31", "2025-01-01", :>)
647
- expect(result).to be true
648
- end
649
-
650
- it "returns false for invalid dates" do
651
- result = described_class.compare_dates("invalid", "2025-01-01", :<)
652
- expect(result).to be false
653
- end
654
- end
655
-
656
- describe ".normalize_day_of_week" do
657
- it "normalizes numeric day" do
658
- result = described_class.normalize_day_of_week(1)
659
- expect(result).to eq(1)
660
- end
661
-
662
- it "normalizes string day" do
663
- result = described_class.normalize_day_of_week("monday")
664
- expect(result).to eq(1)
665
- end
666
-
667
- it "normalizes abbreviated day" do
668
- result = described_class.normalize_day_of_week("mon")
669
- expect(result).to eq(1)
670
- end
671
-
672
- it "returns nil for invalid day" do
673
- result = described_class.normalize_day_of_week("invalid")
674
- expect(result).to be_nil
675
- end
676
- end
677
-
678
- describe ".parse_coordinates" do
679
- it "parses hash with lat/lon" do
680
- result = described_class.parse_coordinates({ lat: 40.7128, lon: -74.0060 })
681
- expect(result).to eq({ lat: 40.7128, lon: -74.0060 })
682
- end
683
-
684
- it "parses hash with latitude/longitude" do
685
- result = described_class.parse_coordinates({ latitude: 40.7128, longitude: -74.0060 })
686
- expect(result).to eq({ lat: 40.7128, lon: -74.0060 })
687
- end
688
-
689
- it "parses array format" do
690
- result = described_class.parse_coordinates([40.7128, -74.0060])
691
- expect(result).to eq({ lat: 40.7128, lon: -74.0060 })
692
- end
693
-
694
- it "returns nil for invalid hash" do
695
- result = described_class.parse_coordinates({ lat: 40.7128 })
696
- expect(result).to be_nil
697
- end
698
-
699
- it "returns nil for invalid array" do
700
- result = described_class.parse_coordinates([40.7128])
701
- expect(result).to be_nil
702
- end
703
- end
704
-
705
- describe ".parse_radius_params" do
706
- it "parses radius parameters" do
707
- params = {
708
- "center" => { "lat" => 40.7128, "lon" => -74.0060 },
709
- "radius" => 10
710
- }
711
- result = described_class.parse_radius_params(params)
712
- expect(result[:center]).to eq({ lat: 40.7128, lon: -74.0060 })
713
- expect(result[:radius]).to eq(10.0)
714
- end
715
-
716
- it "returns nil for invalid params" do
717
- result = described_class.parse_radius_params({ center: { lat: 40.7128 } })
718
- expect(result).to be_nil
719
- end
720
- end
721
-
722
- describe ".parse_polygon" do
723
- it "parses polygon vertices" do
724
- vertices = [
725
- { lat: -1, lon: -1 },
726
- { lat: 1, lon: -1 },
727
- { lat: 1, lon: 1 }
728
- ]
729
- result = described_class.parse_polygon(vertices)
730
- expect(result.size).to eq(3)
731
- end
732
-
733
- it "returns nil for non-array" do
734
- result = described_class.parse_polygon("not an array")
735
- expect(result).to be_nil
736
- end
737
- end
738
-
739
- describe ".haversine_distance" do
740
- it "calculates distance between two points" do
741
- point1 = { lat: 40.7128, lon: -74.0060 }
742
- point2 = { lat: 40.7200, lon: -74.0000 }
743
- result = described_class.haversine_distance(point1, point2)
744
- expect(result).to be_a(Numeric)
745
- expect(result).to be >= 0
746
- end
747
- end
748
-
749
- describe ".point_in_polygon?" do
750
- it "detects point inside polygon" do
751
- point = { lat: 0, lon: 0 }
752
- polygon = [
753
- { lat: -1, lon: -1 },
754
- { lat: 1, lon: -1 },
755
- { lat: 1, lon: 1 },
756
- { lat: -1, lon: 1 }
757
- ]
758
- result = described_class.point_in_polygon?(point, polygon)
759
- expect(result).to be true
760
- end
761
-
762
- it "detects point outside polygon" do
763
- point = { lat: 5, lon: 5 }
764
- polygon = [
765
- { lat: -1, lon: -1 },
766
- { lat: 1, lon: -1 },
767
- { lat: 1, lon: 1 },
768
- { lat: -1, lon: 1 }
769
- ]
770
- result = described_class.point_in_polygon?(point, polygon)
771
- expect(result).to be false
772
- end
773
- end
774
- end