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,247 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "decision_agent/monitoring/storage/memory_adapter"
5
-
6
- RSpec.describe DecisionAgent::Monitoring::Storage::MemoryAdapter do
7
- let(:adapter) { described_class.new(window_size: 3600) }
8
-
9
- describe ".available?" do
10
- it "is always available" do
11
- expect(described_class.available?).to be true
12
- end
13
- end
14
-
15
- describe "#record_decision" do
16
- it "stores decision in memory" do
17
- expect do
18
- adapter.record_decision(
19
- "approve_loan",
20
- { user_id: 123, amount: 10_000 },
21
- confidence: 0.85,
22
- evaluations_count: 3,
23
- duration_ms: 45.5,
24
- status: "success"
25
- )
26
- end.to change { adapter.metrics_count[:decisions] }.by(1)
27
- end
28
- end
29
-
30
- describe "#record_evaluation" do
31
- it "stores evaluation in memory" do
32
- expect do
33
- adapter.record_evaluation(
34
- "CreditScoreEvaluator",
35
- score: 0.92,
36
- success: true,
37
- duration_ms: 12.3,
38
- details: { credit_score: 750 }
39
- )
40
- end.to change { adapter.metrics_count[:evaluations] }.by(1)
41
- end
42
- end
43
-
44
- describe "#record_performance" do
45
- it "stores performance metric in memory" do
46
- expect do
47
- adapter.record_performance(
48
- "database_query",
49
- duration_ms: 150.5,
50
- status: "success",
51
- metadata: { query: "SELECT * FROM users" }
52
- )
53
- end.to change { adapter.metrics_count[:performance] }.by(1)
54
- end
55
- end
56
-
57
- describe "#record_error" do
58
- it "stores error in memory" do
59
- expect do
60
- adapter.record_error(
61
- "ArgumentError",
62
- message: "Invalid input",
63
- stack_trace: ["line 1", "line 2"],
64
- severity: "medium",
65
- context: { input: "bad_value" }
66
- )
67
- end.to change { adapter.metrics_count[:errors] }.by(1)
68
- end
69
- end
70
-
71
- describe "#statistics" do
72
- before do
73
- # Create test data
74
- 5.times do |i|
75
- adapter.record_decision(
76
- "decision_#{i}",
77
- { index: i },
78
- confidence: 0.5 + (i * 0.05),
79
- evaluations_count: 2,
80
- duration_ms: 100,
81
- status: i.even? ? "success" : "failure"
82
- )
83
- end
84
-
85
- 3.times do |i|
86
- adapter.record_evaluation(
87
- "Evaluator#{i}",
88
- score: 0.8 + (i * 0.05),
89
- success: true
90
- )
91
- end
92
-
93
- 6.times do |i|
94
- adapter.record_performance(
95
- "operation",
96
- duration_ms: 100 + (i * 20),
97
- status: "success"
98
- )
99
- end
100
-
101
- 2.times do
102
- adapter.record_error("RuntimeError", severity: "critical")
103
- end
104
- end
105
-
106
- it "returns comprehensive statistics" do
107
- stats = adapter.statistics(time_range: 3600)
108
-
109
- expect(stats[:decisions][:total]).to eq(5)
110
- expect(stats[:decisions][:average_confidence]).to be_within(0.01).of(0.6)
111
- expect(stats[:decisions][:success_rate]).to eq(0.6) # 3 out of 5
112
-
113
- expect(stats[:evaluations][:total]).to eq(3)
114
- expect(stats[:evaluations][:average_score]).to be_within(0.01).of(0.85)
115
-
116
- expect(stats[:performance][:total]).to eq(6)
117
- expect(stats[:performance][:average_duration_ms]).to eq(150.0)
118
- expect(stats[:performance][:success_rate]).to eq(1.0)
119
-
120
- expect(stats[:errors][:total]).to eq(2)
121
- expect(stats[:errors][:critical_count]).to eq(2)
122
- end
123
-
124
- it "filters by time range" do
125
- # Record an old metric that should be filtered out
126
- adapter.instance_variable_get(:@metrics)[:decisions] << {
127
- decision: "old_decision",
128
- confidence: 0.5,
129
- timestamp: Time.now - 7200 # 2 hours ago
130
- }
131
-
132
- stats = adapter.statistics(time_range: 3600) # Last hour only
133
-
134
- expect(stats[:decisions][:total]).to eq(5) # Doesn't include the old one
135
- end
136
- end
137
-
138
- describe "#time_series" do
139
- before do
140
- # Create metrics at different times
141
- now = Time.now
142
- adapter.instance_variable_get(:@metrics)[:decisions] << { timestamp: now - 120 }
143
- adapter.instance_variable_get(:@metrics)[:decisions] << { timestamp: now - 70 }
144
- adapter.instance_variable_get(:@metrics)[:decisions] << { timestamp: now - 10 }
145
- end
146
-
147
- it "groups metrics into time buckets" do
148
- series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
149
-
150
- expect(series[:timestamps]).to be_an(Array)
151
- expect(series[:data]).to be_an(Array)
152
- expect(series[:data].sum).to eq(3) # All 3 metrics
153
- end
154
-
155
- it "uses correct bucket size" do
156
- series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
157
-
158
- # Metrics should be grouped into 60-second buckets
159
- expect(series[:data].max).to be <= 2 # No bucket should have more than 2
160
- end
161
- end
162
-
163
- describe "#metrics_count" do
164
- before do
165
- adapter.record_decision("test", {}, confidence: 0.8)
166
- adapter.record_decision("test2", {}, confidence: 0.9)
167
- adapter.record_evaluation("eval1", score: 0.85)
168
- adapter.record_performance("perf1", duration_ms: 100)
169
- adapter.record_error("Error1")
170
- end
171
-
172
- it "returns count for each metric type" do
173
- counts = adapter.metrics_count
174
-
175
- expect(counts[:decisions]).to eq(2)
176
- expect(counts[:evaluations]).to eq(1)
177
- expect(counts[:performance]).to eq(1)
178
- expect(counts[:errors]).to eq(1)
179
- end
180
- end
181
-
182
- describe "#cleanup" do
183
- let(:long_window_adapter) { described_class.new(window_size: 30 * 24 * 3_600) } # 30 day window
184
-
185
- before do
186
- now = Time.now
187
-
188
- # Add old metrics (8 days ago) to adapter with long window
189
- long_window_adapter.instance_variable_get(:@metrics)[:decisions] << {
190
- decision: "old",
191
- timestamp: now - (8 * 24 * 3600)
192
- }
193
- long_window_adapter.instance_variable_get(:@metrics)[:evaluations] << {
194
- evaluator_name: "old",
195
- timestamp: now - (8 * 24 * 3600)
196
- }
197
-
198
- # Add recent metrics
199
- long_window_adapter.record_decision("recent", {}, confidence: 0.8)
200
- long_window_adapter.record_evaluation("recent", score: 0.9)
201
- end
202
-
203
- it "removes old metrics and returns count" do
204
- count = long_window_adapter.cleanup(older_than: 7 * 24 * 3600) # 7 days
205
-
206
- expect(count).to eq(2) # 2 old metrics removed
207
- expect(long_window_adapter.metrics_count[:decisions]).to eq(1) # Only recent one
208
- expect(long_window_adapter.metrics_count[:evaluations]).to eq(1)
209
- end
210
- end
211
-
212
- describe "window-based cleanup" do
213
- let(:short_window_adapter) { described_class.new(window_size: 60) } # 1 minute window
214
-
215
- it "automatically removes metrics older than window_size" do
216
- now = Time.now
217
-
218
- # Add old metric
219
- short_window_adapter.instance_variable_get(:@metrics)[:decisions] << {
220
- decision: "old",
221
- timestamp: now - 120 # 2 minutes ago
222
- }
223
-
224
- # Add new metric (this should trigger cleanup)
225
- short_window_adapter.record_decision("new", {}, confidence: 0.8)
226
-
227
- # Only the new metric should remain
228
- expect(short_window_adapter.metrics_count[:decisions]).to eq(1)
229
- end
230
- end
231
-
232
- describe "thread safety" do
233
- it "handles concurrent writes" do
234
- threads = 10.times.map do
235
- Thread.new do
236
- 100.times do |i|
237
- adapter.record_decision("concurrent_#{i}", {}, confidence: 0.8)
238
- end
239
- end
240
- end
241
-
242
- threads.each(&:join)
243
-
244
- expect(adapter.metrics_count[:decisions]).to eq(1000)
245
- end
246
- end
247
- end