decision_agent 0.3.0 → 1.1.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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,353 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe "Edge Cases" do
4
- describe "missing fields in context" do
5
- it "handles missing fields gracefully in rule evaluation" do
6
- rules = {
7
- version: "1.0",
8
- ruleset: "test",
9
- rules: [
10
- {
11
- id: "rule_1",
12
- if: { field: "missing_field", op: "eq", value: "value" },
13
- then: { decision: "approve" }
14
- }
15
- ]
16
- }
17
-
18
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
19
- context = DecisionAgent::Context.new({})
20
-
21
- evaluation = evaluator.evaluate(context)
22
-
23
- expect(evaluation).to be_nil
24
- end
25
-
26
- it "handles nil values in comparisons" do
27
- rules = {
28
- version: "1.0",
29
- ruleset: "test",
30
- rules: [
31
- {
32
- id: "rule_1",
33
- if: { field: "value", op: "gt", value: 10 },
34
- then: { decision: "approve" }
35
- }
36
- ]
37
- }
38
-
39
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
40
- context = DecisionAgent::Context.new({ value: nil })
41
-
42
- evaluation = evaluator.evaluate(context)
43
-
44
- expect(evaluation).to be_nil
45
- end
46
- end
47
-
48
- describe "confidence edge cases" do
49
- it "raises error when confidence exceeds 1.0" do
50
- expect do
51
- DecisionAgent::Decision.new(
52
- decision: "test",
53
- confidence: 1.5,
54
- explanations: [],
55
- evaluations: [],
56
- audit_payload: {}
57
- )
58
- end.to raise_error(DecisionAgent::InvalidConfidenceError)
59
- end
60
-
61
- it "raises error when confidence is negative" do
62
- expect do
63
- DecisionAgent::Decision.new(
64
- decision: "test",
65
- confidence: -0.1,
66
- explanations: [],
67
- evaluations: [],
68
- audit_payload: {}
69
- )
70
- end.to raise_error(DecisionAgent::InvalidConfidenceError)
71
- end
72
-
73
- it "accepts confidence at boundary values" do
74
- decision0 = DecisionAgent::Decision.new(
75
- decision: "test",
76
- confidence: 0.0,
77
- explanations: [],
78
- evaluations: [],
79
- audit_payload: {}
80
- )
81
-
82
- decision1 = DecisionAgent::Decision.new(
83
- decision: "test",
84
- confidence: 1.0,
85
- explanations: [],
86
- evaluations: [],
87
- audit_payload: {}
88
- )
89
-
90
- expect(decision0.confidence).to eq(0.0)
91
- expect(decision1.confidence).to eq(1.0)
92
- end
93
- end
94
-
95
- describe "weight edge cases" do
96
- it "raises error when weight exceeds 1.0" do
97
- expect do
98
- DecisionAgent::Evaluation.new(
99
- decision: "test",
100
- weight: 1.5,
101
- reason: "test",
102
- evaluator_name: "test"
103
- )
104
- end.to raise_error(DecisionAgent::InvalidWeightError)
105
- end
106
-
107
- it "raises error when weight is negative" do
108
- expect do
109
- DecisionAgent::Evaluation.new(
110
- decision: "test",
111
- weight: -0.1,
112
- reason: "test",
113
- evaluator_name: "test"
114
- )
115
- end.to raise_error(DecisionAgent::InvalidWeightError)
116
- end
117
-
118
- it "accepts weight at boundary values" do
119
- eval0 = DecisionAgent::Evaluation.new(
120
- decision: "test",
121
- weight: 0.0,
122
- reason: "test",
123
- evaluator_name: "test"
124
- )
125
-
126
- eval1 = DecisionAgent::Evaluation.new(
127
- decision: "test",
128
- weight: 1.0,
129
- reason: "test",
130
- evaluator_name: "test"
131
- )
132
-
133
- expect(eval0.weight).to eq(0.0)
134
- expect(eval1.weight).to eq(1.0)
135
- end
136
- end
137
-
138
- describe "empty arrays and collections" do
139
- it "handles rules with empty 'all' conditions" do
140
- rules = {
141
- version: "1.0",
142
- ruleset: "test",
143
- rules: [
144
- {
145
- id: "rule_1",
146
- if: { all: [] },
147
- then: { decision: "approve" }
148
- }
149
- ]
150
- }
151
-
152
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
153
- context = DecisionAgent::Context.new({})
154
-
155
- evaluation = evaluator.evaluate(context)
156
-
157
- expect(evaluation).not_to be_nil
158
- end
159
-
160
- it "handles rules with empty 'any' conditions" do
161
- rules = {
162
- version: "1.0",
163
- ruleset: "test",
164
- rules: [
165
- {
166
- id: "rule_1",
167
- if: { any: [] },
168
- then: { decision: "approve" }
169
- }
170
- ]
171
- }
172
-
173
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
174
- context = DecisionAgent::Context.new({})
175
-
176
- evaluation = evaluator.evaluate(context)
177
-
178
- expect(evaluation).to be_nil
179
- end
180
- end
181
-
182
- describe "type mismatches in comparisons" do
183
- it "handles type mismatches in numeric comparisons" do
184
- rules = {
185
- version: "1.0",
186
- ruleset: "test",
187
- rules: [
188
- {
189
- id: "rule_1",
190
- if: { field: "value", op: "gt", value: 10 },
191
- then: { decision: "approve" }
192
- }
193
- ]
194
- }
195
-
196
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
197
- context = DecisionAgent::Context.new({ value: "not_a_number" })
198
-
199
- evaluation = evaluator.evaluate(context)
200
-
201
- expect(evaluation).to be_nil
202
- end
203
- end
204
-
205
- describe "immutability" do
206
- it "freezes context data to prevent modification" do
207
- context = DecisionAgent::Context.new({ user: "alice" })
208
-
209
- expect do
210
- context.to_h[:user] = "bob"
211
- end.to raise_error(FrozenError)
212
- end
213
-
214
- it "freezes evaluation fields" do
215
- evaluation = DecisionAgent::Evaluation.new(
216
- decision: "approve",
217
- weight: 0.8,
218
- reason: "test",
219
- evaluator_name: "test"
220
- )
221
-
222
- expect(evaluation.decision).to be_frozen
223
- expect(evaluation.reason).to be_frozen
224
- expect(evaluation.evaluator_name).to be_frozen
225
- end
226
-
227
- it "freezes decision fields" do
228
- decision = DecisionAgent::Decision.new(
229
- decision: "approve",
230
- confidence: 0.8,
231
- explanations: ["test"],
232
- evaluations: [],
233
- audit_payload: {}
234
- )
235
-
236
- expect(decision.decision).to be_frozen
237
- expect(decision.explanations).to be_frozen
238
- end
239
- end
240
-
241
- describe "special characters and unicode" do
242
- it "handles unicode in context values" do
243
- context = DecisionAgent::Context.new({ user: "用户", message: "Hello 世界" })
244
-
245
- evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
246
- agent = DecisionAgent::Agent.new(evaluators: [evaluator])
247
-
248
- result = agent.decide(context: context)
249
-
250
- expect(result.audit_payload[:context][:user]).to eq("用户")
251
- end
252
-
253
- it "handles special characters in rule values" do
254
- rules = {
255
- version: "1.0",
256
- ruleset: "test",
257
- rules: [
258
- {
259
- id: "rule_1",
260
- if: { field: "symbol", op: "eq", value: "@#$%^&*()" },
261
- then: { decision: "special" }
262
- }
263
- ]
264
- }
265
-
266
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
267
- context = DecisionAgent::Context.new({ symbol: "@#$%^&*()" })
268
-
269
- evaluation = evaluator.evaluate(context)
270
-
271
- expect(evaluation).not_to be_nil
272
- expect(evaluation.decision).to eq("special")
273
- end
274
- end
275
-
276
- describe "very large numbers and values" do
277
- it "handles large numeric values in comparisons" do
278
- rules = {
279
- version: "1.0",
280
- ruleset: "test",
281
- rules: [
282
- {
283
- id: "rule_1",
284
- if: { field: "amount", op: "gte", value: 1_000_000_000 },
285
- then: { decision: "large_amount" }
286
- }
287
- ]
288
- }
289
-
290
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
291
- context = DecisionAgent::Context.new({ amount: 5_000_000_000 })
292
-
293
- evaluation = evaluator.evaluate(context)
294
-
295
- expect(evaluation).not_to be_nil
296
- expect(evaluation.decision).to eq("large_amount")
297
- end
298
- end
299
-
300
- describe "deeply nested context" do
301
- it "handles deeply nested field access" do
302
- rules = {
303
- version: "1.0",
304
- ruleset: "test",
305
- rules: [
306
- {
307
- id: "rule_1",
308
- if: { field: "a.b.c.d.e", op: "eq", value: "deep" },
309
- then: { decision: "found_deep" }
310
- }
311
- ]
312
- }
313
-
314
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
315
- context = DecisionAgent::Context.new({
316
- a: {
317
- b: {
318
- c: {
319
- d: {
320
- e: "deep"
321
- }
322
- }
323
- }
324
- }
325
- })
326
-
327
- evaluation = evaluator.evaluate(context)
328
-
329
- expect(evaluation).not_to be_nil
330
- expect(evaluation.decision).to eq("found_deep")
331
- end
332
- end
333
-
334
- describe "audit adapter errors" do
335
- it "propagates errors from audit adapter" do
336
- failing_adapter = Class.new(DecisionAgent::Audit::Adapter) do
337
- def record(_decision, _context)
338
- raise StandardError, "Audit failed"
339
- end
340
- end
341
-
342
- evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
343
- agent = DecisionAgent::Agent.new(
344
- evaluators: [evaluator],
345
- audit_adapter: failing_adapter.new
346
- )
347
-
348
- expect do
349
- agent.decide(context: {})
350
- end.to raise_error(StandardError, "Audit failed")
351
- end
352
- end
353
- end
@@ -1,364 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe DecisionAgent::Evaluation do
4
- describe "#initialize" do
5
- it "creates an evaluation with all required fields" do
6
- evaluation = described_class.new(
7
- decision: "approve",
8
- weight: 0.8,
9
- reason: "Test reason",
10
- evaluator_name: "TestEvaluator"
11
- )
12
-
13
- expect(evaluation.decision).to eq("approve")
14
- expect(evaluation.weight).to eq(0.8)
15
- expect(evaluation.reason).to eq("Test reason")
16
- expect(evaluation.evaluator_name).to eq("TestEvaluator")
17
- expect(evaluation.metadata).to eq({})
18
- end
19
-
20
- it "converts decision to string" do
21
- evaluation = described_class.new(
22
- decision: :approve,
23
- weight: 0.8,
24
- reason: "Test",
25
- evaluator_name: "Test"
26
- )
27
-
28
- expect(evaluation.decision).to eq("approve")
29
- end
30
-
31
- it "converts weight to float" do
32
- evaluation = described_class.new(
33
- decision: "approve",
34
- weight: "0.8",
35
- reason: "Test",
36
- evaluator_name: "Test"
37
- )
38
-
39
- expect(evaluation.weight).to eq(0.8)
40
- end
41
-
42
- it "converts reason to string" do
43
- evaluation = described_class.new(
44
- decision: "approve",
45
- weight: 0.8,
46
- reason: :test_reason,
47
- evaluator_name: "Test"
48
- )
49
-
50
- expect(evaluation.reason).to eq("test_reason")
51
- end
52
-
53
- it "converts evaluator_name to string" do
54
- evaluation = described_class.new(
55
- decision: "approve",
56
- weight: 0.8,
57
- reason: "Test",
58
- evaluator_name: :TestEvaluator
59
- )
60
-
61
- expect(evaluation.evaluator_name).to eq("TestEvaluator")
62
- end
63
-
64
- it "freezes the evaluation object" do
65
- evaluation = described_class.new(
66
- decision: "approve",
67
- weight: 0.8,
68
- reason: "Test",
69
- evaluator_name: "Test"
70
- )
71
-
72
- expect(evaluation).to be_frozen
73
- end
74
-
75
- it "freezes nested structures" do
76
- evaluation = described_class.new(
77
- decision: "approve",
78
- weight: 0.8,
79
- reason: "Test",
80
- evaluator_name: "Test",
81
- metadata: { key: "value", nested: { data: [1, 2, 3] } }
82
- )
83
-
84
- expect(evaluation.decision).to be_frozen
85
- expect(evaluation.reason).to be_frozen
86
- expect(evaluation.evaluator_name).to be_frozen
87
- expect(evaluation.metadata).to be_frozen
88
- expect(evaluation.metadata[:nested]).to be_frozen
89
- expect(evaluation.metadata[:nested][:data]).to be_frozen
90
- end
91
-
92
- it "freezes metadata in-place without creating new objects" do
93
- original_metadata = { key: "value", nested: { data: [1, 2, 3] } }
94
- original_metadata_id = original_metadata.object_id
95
- original_nested_id = original_metadata[:nested].object_id
96
-
97
- evaluation = described_class.new(
98
- decision: "approve",
99
- weight: 0.8,
100
- reason: "Test",
101
- evaluator_name: "Test",
102
- metadata: original_metadata
103
- )
104
-
105
- # Should freeze in-place, not create new objects
106
- expect(evaluation.metadata.object_id).to eq(original_metadata_id)
107
- expect(evaluation.metadata[:nested].object_id).to eq(original_nested_id)
108
- expect(evaluation.metadata).to be_frozen
109
- expect(evaluation.metadata[:nested]).to be_frozen
110
- end
111
-
112
- it "skips already frozen objects in deep_freeze" do
113
- frozen_metadata = { key: "value", nested: { data: [1, 2, 3] } }
114
- frozen_metadata.freeze
115
- frozen_metadata[:nested].freeze
116
-
117
- evaluation = described_class.new(
118
- decision: "approve",
119
- weight: 0.8,
120
- reason: "Test",
121
- evaluator_name: "Test",
122
- metadata: frozen_metadata
123
- )
124
-
125
- expect(evaluation.metadata).to be_frozen
126
- expect(evaluation.metadata[:nested]).to be_frozen
127
- end
128
-
129
- it "does not freeze hash keys unnecessarily" do
130
- key_symbol = :test_key
131
- key_string = "test_key"
132
- metadata = {
133
- key_symbol => "value1",
134
- key_string => "value2"
135
- }
136
-
137
- evaluation = described_class.new(
138
- decision: "approve",
139
- weight: 0.8,
140
- reason: "Test",
141
- evaluator_name: "Test",
142
- metadata: metadata
143
- )
144
-
145
- # Keys should not be frozen (they're typically symbols/strings that don't need freezing)
146
- expect(evaluation.metadata.keys.first).to eq(key_symbol)
147
- expect(evaluation.metadata.keys.last).to eq(key_string)
148
- # Values should be frozen
149
- expect(evaluation.metadata[key_symbol]).to be_frozen
150
- expect(evaluation.metadata[key_string]).to be_frozen
151
- end
152
-
153
- it "raises error for weight outside 0-1 range" do
154
- expect do
155
- described_class.new(
156
- decision: "approve",
157
- weight: 1.5,
158
- reason: "Test",
159
- evaluator_name: "Test"
160
- )
161
- end.to raise_error(DecisionAgent::InvalidWeightError)
162
- end
163
-
164
- it "raises error for negative weight" do
165
- expect do
166
- described_class.new(
167
- decision: "approve",
168
- weight: -0.1,
169
- reason: "Test",
170
- evaluator_name: "Test"
171
- )
172
- end.to raise_error(DecisionAgent::InvalidWeightError)
173
- end
174
-
175
- it "accepts weight at boundaries" do
176
- eval1 = described_class.new(
177
- decision: "approve",
178
- weight: 0.0,
179
- reason: "Test",
180
- evaluator_name: "Test"
181
- )
182
- expect(eval1.weight).to eq(0.0)
183
-
184
- eval2 = described_class.new(
185
- decision: "approve",
186
- weight: 1.0,
187
- reason: "Test",
188
- evaluator_name: "Test"
189
- )
190
- expect(eval2.weight).to eq(1.0)
191
- end
192
-
193
- it "handles metadata" do
194
- metadata = { rule_id: "rule_1", source: "test" }
195
- evaluation = described_class.new(
196
- decision: "approve",
197
- weight: 0.8,
198
- reason: "Test",
199
- evaluator_name: "Test",
200
- metadata: metadata
201
- )
202
-
203
- expect(evaluation.metadata).to eq(metadata)
204
- end
205
-
206
- it "defaults to empty metadata" do
207
- evaluation = described_class.new(
208
- decision: "approve",
209
- weight: 0.8,
210
- reason: "Test",
211
- evaluator_name: "Test"
212
- )
213
-
214
- expect(evaluation.metadata).to eq({})
215
- end
216
- end
217
-
218
- describe "#to_h" do
219
- it "converts evaluation to hash" do
220
- evaluation = described_class.new(
221
- decision: "approve",
222
- weight: 0.8,
223
- reason: "Test reason",
224
- evaluator_name: "TestEvaluator",
225
- metadata: { key: "value" }
226
- )
227
-
228
- hash = evaluation.to_h
229
-
230
- expect(hash).to be_a(Hash)
231
- expect(hash[:decision]).to eq("approve")
232
- expect(hash[:weight]).to eq(0.8)
233
- expect(hash[:reason]).to eq("Test reason")
234
- expect(hash[:evaluator_name]).to eq("TestEvaluator")
235
- expect(hash[:metadata]).to eq({ key: "value" })
236
- end
237
- end
238
-
239
- describe "#==" do
240
- it "compares evaluations by all fields" do
241
- eval1 = described_class.new(
242
- decision: "approve",
243
- weight: 0.8,
244
- reason: "Test",
245
- evaluator_name: "Test",
246
- metadata: { key: "value" }
247
- )
248
-
249
- eval2 = described_class.new(
250
- decision: "approve",
251
- weight: 0.8,
252
- reason: "Test",
253
- evaluator_name: "Test",
254
- metadata: { key: "value" }
255
- )
256
-
257
- expect(eval1).to eq(eval2)
258
- end
259
-
260
- it "returns false for different decisions" do
261
- eval1 = described_class.new(
262
- decision: "approve",
263
- weight: 0.8,
264
- reason: "Test",
265
- evaluator_name: "Test"
266
- )
267
-
268
- eval2 = described_class.new(
269
- decision: "reject",
270
- weight: 0.8,
271
- reason: "Test",
272
- evaluator_name: "Test"
273
- )
274
-
275
- expect(eval1).not_to eq(eval2)
276
- end
277
-
278
- it "returns false for different weights" do
279
- eval1 = described_class.new(
280
- decision: "approve",
281
- weight: 0.8,
282
- reason: "Test",
283
- evaluator_name: "Test"
284
- )
285
-
286
- eval2 = described_class.new(
287
- decision: "approve",
288
- weight: 0.9,
289
- reason: "Test",
290
- evaluator_name: "Test"
291
- )
292
-
293
- expect(eval1).not_to eq(eval2)
294
- end
295
-
296
- it "returns false for different reasons" do
297
- eval1 = described_class.new(
298
- decision: "approve",
299
- weight: 0.8,
300
- reason: "Reason 1",
301
- evaluator_name: "Test"
302
- )
303
-
304
- eval2 = described_class.new(
305
- decision: "approve",
306
- weight: 0.8,
307
- reason: "Reason 2",
308
- evaluator_name: "Test"
309
- )
310
-
311
- expect(eval1).not_to eq(eval2)
312
- end
313
-
314
- it "returns false for different evaluator names" do
315
- eval1 = described_class.new(
316
- decision: "approve",
317
- weight: 0.8,
318
- reason: "Test",
319
- evaluator_name: "Evaluator1"
320
- )
321
-
322
- eval2 = described_class.new(
323
- decision: "approve",
324
- weight: 0.8,
325
- reason: "Test",
326
- evaluator_name: "Evaluator2"
327
- )
328
-
329
- expect(eval1).not_to eq(eval2)
330
- end
331
-
332
- it "returns false for different metadata" do
333
- eval1 = described_class.new(
334
- decision: "approve",
335
- weight: 0.8,
336
- reason: "Test",
337
- evaluator_name: "Test",
338
- metadata: { key: "value1" }
339
- )
340
-
341
- eval2 = described_class.new(
342
- decision: "approve",
343
- weight: 0.8,
344
- reason: "Test",
345
- evaluator_name: "Test",
346
- metadata: { key: "value2" }
347
- )
348
-
349
- expect(eval1).not_to eq(eval2)
350
- end
351
-
352
- it "returns false for non-Evaluation objects" do
353
- evaluation = described_class.new(
354
- decision: "approve",
355
- weight: 0.8,
356
- reason: "Test",
357
- evaluator_name: "Test"
358
- )
359
-
360
- expect(evaluation).not_to eq("not an evaluation")
361
- expect(evaluation).not_to eq(nil)
362
- end
363
- end
364
- end