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,490 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- RSpec.describe "Thread-Safety" do
6
- describe "Agent with shared evaluators" do
7
- let(:rules_json) do
8
- {
9
- version: "1.0",
10
- ruleset: "approval_rules",
11
- rules: [
12
- {
13
- id: "approve_high",
14
- if: { field: "amount", op: "gt", value: 1000 },
15
- then: { decision: "approve", weight: 0.9, reason: "High value" }
16
- },
17
- {
18
- id: "reject_low",
19
- if: { field: "amount", op: "lte", value: 1000 },
20
- then: { decision: "reject", weight: 0.8, reason: "Low value" }
21
- }
22
- ]
23
- }
24
- end
25
-
26
- let(:evaluator) { DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json) }
27
- let(:agent) { DecisionAgent::Agent.new(evaluators: [evaluator]) }
28
-
29
- it "handles concurrent decisions from multiple threads safely" do
30
- threads = []
31
- results = Array.new(50)
32
-
33
- # Create 50 threads making concurrent decisions
34
- 50.times do |i|
35
- threads << Thread.new do
36
- context = { amount: i.even? ? 1500 : 500 }
37
- results[i] = agent.decide(context: context)
38
- end
39
- end
40
-
41
- threads.each(&:join)
42
-
43
- # Verify all threads completed successfully
44
- expect(results.compact.size).to eq(50)
45
-
46
- # Verify results are correct and frozen
47
- results.each_with_index do |decision, i|
48
- expect(decision).to be_frozen
49
- expect(decision.decision).to be_frozen
50
- expect(decision.explanations).to be_frozen
51
- expect(decision.evaluations).to be_frozen
52
- expect(decision.audit_payload).to be_frozen
53
-
54
- # Verify correctness based on input
55
- if i.even?
56
- expect(decision.decision).to eq("approve")
57
- else
58
- expect(decision.decision).to eq("reject")
59
- end
60
- end
61
- end
62
-
63
- it "prevents modification of shared evaluator ruleset" do
64
- # Verify the ruleset is frozen
65
- expect(evaluator.instance_variable_get(:@ruleset)).to be_frozen
66
-
67
- # Attempt to modify should raise error
68
- expect do
69
- evaluator.instance_variable_get(:@ruleset)["rules"] << { id: "new_rule" }
70
- end.to raise_error(FrozenError)
71
- end
72
-
73
- it "prevents modification of evaluators array in Agent" do
74
- expect(agent.evaluators).to be_frozen
75
-
76
- expect do
77
- agent.evaluators << DecisionAgent::Evaluators::StaticEvaluator.new(decision: true, weight: 1.0)
78
- end.to raise_error(FrozenError)
79
- end
80
- end
81
-
82
- describe "Multiple agents sharing evaluators" do
83
- let(:evaluator) do
84
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(
85
- rules_json: {
86
- version: "1.0",
87
- ruleset: "shared_rules",
88
- rules: [
89
- {
90
- id: "rule1",
91
- if: { field: "value", op: "eq", value: "yes" },
92
- then: { decision: "approve", weight: 1.0, reason: "Match" }
93
- }
94
- ]
95
- }
96
- )
97
- end
98
-
99
- it "allows multiple agents to safely share the same evaluator instance" do
100
- agent1 = DecisionAgent::Agent.new(evaluators: [evaluator])
101
- agent2 = DecisionAgent::Agent.new(evaluators: [evaluator])
102
- agent3 = DecisionAgent::Agent.new(evaluators: [evaluator])
103
-
104
- results = []
105
- mutex = Mutex.new
106
-
107
- # Each agent makes decisions in parallel
108
- threads = [agent1, agent2, agent3].map do |agent|
109
- Thread.new do
110
- 10.times do
111
- decision = agent.decide(context: { value: "yes" })
112
- mutex.synchronize { results << decision }
113
- end
114
- end
115
- end
116
-
117
- threads.each(&:join)
118
-
119
- # All 30 decisions should succeed
120
- expect(results.size).to eq(30)
121
- results.each do |decision|
122
- expect(decision.decision).to eq("approve")
123
- expect(decision).to be_frozen
124
- end
125
- end
126
- end
127
-
128
- describe "Evaluation immutability" do
129
- it "ensures evaluations are deeply frozen" do
130
- evaluation = DecisionAgent::Evaluation.new(
131
- decision: "approve",
132
- weight: 0.8,
133
- reason: "Test reason",
134
- evaluator_name: "TestEvaluator",
135
- metadata: { key: "value" }
136
- )
137
-
138
- expect(evaluation).to be_frozen
139
- expect(evaluation.decision).to be_frozen
140
- expect(evaluation.reason).to be_frozen
141
- expect(evaluation.evaluator_name).to be_frozen
142
- expect(evaluation.metadata).to be_frozen
143
- end
144
- end
145
-
146
- describe "Decision immutability" do
147
- it "ensures decisions are deeply frozen" do
148
- evaluation = DecisionAgent::Evaluation.new(
149
- decision: "approve",
150
- weight: 1.0,
151
- reason: "Test",
152
- evaluator_name: "Test"
153
- )
154
-
155
- decision = DecisionAgent::Decision.new(
156
- decision: "approve",
157
- confidence: 0.95,
158
- explanations: ["Explanation 1"],
159
- evaluations: [evaluation],
160
- audit_payload: { timestamp: "2024-01-01" }
161
- )
162
-
163
- expect(decision).to be_frozen
164
- expect(decision.decision).to be_frozen
165
- expect(decision.explanations).to be_frozen
166
- expect(decision.evaluations).to be_frozen
167
- expect(decision.audit_payload).to be_frozen
168
-
169
- # Nested structures should also be frozen
170
- expect(decision.explanations.first).to be_frozen
171
- expect(decision.evaluations.first).to be_frozen
172
- end
173
- end
174
-
175
- describe "Context immutability" do
176
- it "freezes context data to prevent mutation" do
177
- context_data = { user: { id: 1, name: "Test" }, amount: 100 }
178
- context = DecisionAgent::Context.new(context_data)
179
-
180
- expect(context.to_h).to be_frozen
181
- expect(context.to_h[:user]).to be_frozen
182
-
183
- # Original data should not be affected
184
- expect(context_data).not_to be_frozen
185
- end
186
- end
187
-
188
- describe "Concurrent file storage operations" do
189
- let(:storage_path) { File.join(__dir__, "../tmp/thread_safety_test") }
190
- let(:adapter) { DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: storage_path) }
191
-
192
- before do
193
- FileUtils.rm_rf(storage_path)
194
- end
195
-
196
- after do
197
- FileUtils.rm_rf(storage_path)
198
- end
199
-
200
- it "handles concurrent version creation safely" do
201
- threads = []
202
- results = []
203
- mutex = Mutex.new
204
-
205
- # Create 10 versions concurrently
206
- 10.times do |i|
207
- threads << Thread.new do
208
- version = adapter.create_version(
209
- rule_id: "concurrent_rule",
210
- content: { rule: "version_#{i}" },
211
- metadata: { created_by: "thread_#{i}" }
212
- )
213
- mutex.synchronize { results << version }
214
- end
215
- end
216
-
217
- threads.each(&:join)
218
-
219
- # All versions should be created successfully
220
- expect(results.size).to eq(10)
221
-
222
- # Version numbers should be unique and sequential
223
- version_numbers = results.map { |v| v[:version_number] }.sort
224
- expect(version_numbers).to eq((1..10).to_a)
225
-
226
- # Each thread created its version as active
227
- # Due to thread scheduling, all might be created as active initially
228
- # The last one written should be active in the file system
229
- final_active = adapter.get_active_version(rule_id: "concurrent_rule")
230
- expect(final_active).not_to be_nil
231
- expect(final_active[:status]).to eq("active")
232
- end
233
-
234
- it "handles concurrent read and write operations safely" do
235
- # Create initial version
236
- adapter.create_version(
237
- rule_id: "read_write_test",
238
- content: { rule: "initial" },
239
- metadata: { created_by: "setup" }
240
- )
241
-
242
- threads = []
243
- read_results = []
244
- write_results = []
245
- read_mutex = Mutex.new
246
- write_mutex = Mutex.new
247
-
248
- # Mix of read and write operations
249
- 10.times do |i|
250
- threads << if i.even?
251
- # Read operations
252
- Thread.new do
253
- versions = adapter.list_versions(rule_id: "read_write_test")
254
- read_mutex.synchronize { read_results << versions }
255
- end
256
- else
257
- # Write operations
258
- Thread.new do
259
- version = adapter.create_version(
260
- rule_id: "read_write_test",
261
- content: { rule: "version_#{i}" },
262
- metadata: { created_by: "thread_#{i}" }
263
- )
264
- write_mutex.synchronize { write_results << version }
265
- end
266
- end
267
- end
268
-
269
- threads.each(&:join)
270
-
271
- # All operations should complete successfully
272
- expect(read_results.size).to eq(5)
273
- expect(write_results.size).to eq(5)
274
-
275
- # Reads should never return inconsistent data
276
- read_results.each do |versions|
277
- expect(versions).to be_an(Array)
278
- versions.each do |version|
279
- expect(version).to have_key(:id)
280
- expect(version).to have_key(:version_number)
281
- expect(version).to have_key(:status)
282
- end
283
- end
284
- end
285
- end
286
-
287
- describe "EvaluationValidator" do
288
- it "validates frozen evaluations" do
289
- evaluation = DecisionAgent::Evaluation.new(
290
- decision: "approve",
291
- weight: 0.8,
292
- reason: "Valid",
293
- evaluator_name: "TestEvaluator"
294
- )
295
-
296
- expect do
297
- DecisionAgent::EvaluationValidator.validate!(evaluation)
298
- end.not_to raise_error
299
- end
300
-
301
- it "raises error for unfrozen evaluations" do
302
- # NOTE: Evaluation objects are always frozen in their initializer.
303
- # To test the validator's frozen check, we need to create an unfrozen instance.
304
- # Using allocate allows us to bypass the initializer (which would freeze the object)
305
- # and manually set instance variables to create a valid but unfrozen evaluation.
306
- # This tests the edge case where an evaluation might not be frozen (though
307
- # this should never happen in practice with real Evaluation instances).
308
- evaluation = DecisionAgent::Evaluation.allocate
309
- evaluation.instance_variable_set(:@decision, "approve")
310
- evaluation.instance_variable_set(:@weight, 0.8)
311
- evaluation.instance_variable_set(:@reason, "Test")
312
- evaluation.instance_variable_set(:@evaluator_name, "TestEvaluator")
313
-
314
- # Verify it's not frozen (this is the condition we're testing)
315
- expect(evaluation).not_to be_frozen
316
-
317
- expect do
318
- DecisionAgent::EvaluationValidator.validate!(evaluation)
319
- end.to raise_error(DecisionAgent::EvaluationValidator::ValidationError, /must be frozen/)
320
- end
321
-
322
- it "validates arrays of evaluations" do
323
- evaluations = [
324
- DecisionAgent::Evaluation.new(
325
- decision: "approve",
326
- weight: 0.8,
327
- reason: "Valid 1",
328
- evaluator_name: "Evaluator1"
329
- ),
330
- DecisionAgent::Evaluation.new(
331
- decision: "reject",
332
- weight: 0.6,
333
- reason: "Valid 2",
334
- evaluator_name: "Evaluator2"
335
- )
336
- ]
337
-
338
- expect do
339
- DecisionAgent::EvaluationValidator.validate_all!(evaluations)
340
- end.not_to raise_error
341
- end
342
- end
343
-
344
- describe "Stress Testing & Extended Coverage" do
345
- let(:rules_json) do
346
- {
347
- version: "1.0",
348
- ruleset: "stress_test",
349
- rules: [
350
- {
351
- id: "rule1",
352
- if: { field: "value", op: "gt", value: 50 },
353
- then: { decision: "high", weight: 0.9, reason: "High value" }
354
- },
355
- {
356
- id: "rule2",
357
- if: { field: "value", op: "lte", value: 50 },
358
- then: { decision: "low", weight: 0.8, reason: "Low value" }
359
- }
360
- ]
361
- }
362
- end
363
-
364
- let(:evaluator) { DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json) }
365
- let(:agent) { DecisionAgent::Agent.new(evaluators: [evaluator]) }
366
-
367
- it "handles 100 threads making 100 decisions each (10,000 total)" do
368
- thread_count = 100
369
- decisions_per_thread = 100
370
- total_decisions = thread_count * decisions_per_thread
371
- results = []
372
- mutex = Mutex.new
373
-
374
- threads = thread_count.times.map do |thread_id|
375
- Thread.new do
376
- decisions_per_thread.times do |i|
377
- context = { value: ((thread_id * decisions_per_thread) + i) % 100 }
378
- decision = agent.decide(context: context)
379
- mutex.synchronize { results << decision }
380
- end
381
- end
382
- end
383
-
384
- threads.each(&:join)
385
-
386
- expect(results.size).to eq(total_decisions)
387
- expect(results).to all(be_frozen)
388
- expect(results.map(&:decision).uniq.sort).to eq(%w[high low])
389
- end
390
-
391
- it "handles rapid-fire decisions with deterministic results" do
392
- results = []
393
-
394
- 1000.times do |i|
395
- decision = agent.decide(context: { value: i % 100 })
396
- results << decision
397
- end
398
-
399
- expect(results.size).to eq(1000)
400
- expect(results).to all(be_frozen)
401
-
402
- # Verify determinism - same input produces same output
403
- decision1 = agent.decide(context: { value: 75 })
404
- decision2 = agent.decide(context: { value: 75 })
405
- expect(decision1.decision).to eq(decision2.decision)
406
- expect(decision1.confidence).to eq(decision2.confidence)
407
- end
408
-
409
- it "handles concurrent decisions with complex nested contexts" do
410
- complex_contexts = 50.times.map do |i|
411
- {
412
- value: i,
413
- user: {
414
- id: i,
415
- profile: {
416
- age: 20 + (i % 50),
417
- score: 0.5 + ((i % 10) * 0.05)
418
- }
419
- },
420
- metadata: {
421
- tags: ["tag#{i % 5}", "tag#{i % 3}"],
422
- timestamps: [Time.now.to_i - i, Time.now.to_i]
423
- }
424
- }
425
- end
426
-
427
- results = []
428
- mutex = Mutex.new
429
-
430
- threads = complex_contexts.map do |context|
431
- Thread.new do
432
- decision = agent.decide(context: context)
433
- mutex.synchronize { results << decision }
434
- end
435
- end
436
-
437
- threads.each(&:join)
438
-
439
- expect(results.size).to eq(50)
440
- expect(results).to all(be_frozen)
441
- results.each do |decision|
442
- expect(decision.audit_payload).to be_frozen
443
- expect(decision.audit_payload[:context]).to be_frozen
444
- end
445
- end
446
-
447
- it "prevents race conditions when reading same frozen decision" do
448
- results = []
449
- mutex = Mutex.new
450
- decision = agent.decide(context: { value: 0 })
451
-
452
- # Multiple threads reading the same frozen decision
453
- threads = 100.times.map do
454
- Thread.new do
455
- # These reads should be safe because decision is frozen
456
- data = {
457
- decision: decision.decision,
458
- confidence: decision.confidence,
459
- evaluations_count: decision.evaluations.size
460
- }
461
- mutex.synchronize { results << data }
462
- end
463
- end
464
-
465
- threads.each(&:join)
466
-
467
- expect(results.size).to eq(100)
468
- # All threads should see the same values
469
- expect(results.map { |r| r[:decision] }.uniq).to eq(["low"])
470
- expect(results.map { |r| r[:evaluations_count] }.uniq).to eq([1])
471
- end
472
-
473
- it "ensures original context data is not mutated" do
474
- original_context = { value: 75, count: 0 }
475
- original_context_copy = original_context.dup
476
-
477
- threads = 20.times.map do
478
- Thread.new do
479
- agent.decide(context: original_context)
480
- end
481
- end
482
-
483
- threads.each(&:join)
484
-
485
- # Original context should be unchanged
486
- expect(original_context).to eq(original_context_copy)
487
- expect(original_context).not_to be_frozen
488
- end
489
- end
490
- end