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,493 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- RSpec.describe "Performance Optimizations" do
6
- describe "MetricsCollector cleanup batching" do
7
- let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new(window_size: 60, cleanup_threshold: 10) }
8
- let(:evaluator) do
9
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(
10
- rules_json: {
11
- version: "1.0",
12
- ruleset: "test",
13
- rules: [
14
- {
15
- id: "rule1",
16
- if: { field: "amount", op: "gte", value: 0 },
17
- then: { decision: "approve", weight: 1.0, reason: "Test" }
18
- }
19
- ]
20
- }
21
- )
22
- end
23
- let(:agent) { DecisionAgent::Agent.new(evaluators: [evaluator]) }
24
-
25
- it "does not cleanup on every record" do
26
- # Record 5 decisions (below threshold of 10)
27
- 5.times do |i|
28
- decision = agent.decide(context: { amount: i * 100 })
29
- collector.record_decision(decision, DecisionAgent::Context.new({ amount: i * 100 }))
30
- end
31
-
32
- # Should have 5 decisions
33
- expect(collector.metrics[:decisions].size).to eq(5)
34
-
35
- # Record 5 more to cross threshold
36
- 5.times do |i|
37
- decision = agent.decide(context: { amount: i * 100 })
38
- collector.record_decision(decision, DecisionAgent::Context.new({ amount: i * 100 }))
39
- end
40
-
41
- # Cleanup should have been triggered at 10
42
- expect(collector.metrics[:decisions].size).to be <= 10
43
- end
44
-
45
- it "allows configurable cleanup threshold" do
46
- custom_collector = DecisionAgent::Monitoring::MetricsCollector.new(
47
- window_size: 60,
48
- cleanup_threshold: 5
49
- )
50
-
51
- # Record 4 decisions (below threshold)
52
- 4.times do |i|
53
- decision = agent.decide(context: { amount: i * 100 })
54
- custom_collector.record_decision(decision, DecisionAgent::Context.new({ amount: i * 100 }))
55
- end
56
-
57
- expect(custom_collector.metrics[:decisions].size).to eq(4)
58
- end
59
-
60
- it "maintains backward compatibility with default threshold" do
61
- default_collector = DecisionAgent::Monitoring::MetricsCollector.new(window_size: 60)
62
-
63
- # Should work without specifying cleanup_threshold
64
- decision = agent.decide(context: { amount: 100 })
65
- expect do
66
- default_collector.record_decision(decision, DecisionAgent::Context.new({ amount: 100 }))
67
- end.not_to raise_error
68
- end
69
- end
70
-
71
- describe "ABTestingAgent caching" do
72
- let(:storage_adapter) { DecisionAgent::ABTesting::Storage::MemoryAdapter.new }
73
- let(:version_manager) do
74
- DecisionAgent::Versioning::VersionManager.new(
75
- adapter: DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: "./tmp/test_versions")
76
- )
77
- end
78
- let(:ab_test_manager) do
79
- DecisionAgent::ABTesting::ABTestManager.new(
80
- storage_adapter: storage_adapter,
81
- version_manager: version_manager
82
- )
83
- end
84
-
85
- before do
86
- FileUtils.mkdir_p("./tmp/test_versions")
87
-
88
- # Create test versions
89
- @version1 = version_manager.save_version(
90
- rule_id: "test_rule",
91
- rule_content: {
92
- version: "1.0",
93
- ruleset: "champion",
94
- rules: [
95
- {
96
- id: "rule1",
97
- if: { field: "amount", op: "gte", value: 0 },
98
- then: { decision: "approve", weight: 1.0, reason: "Champion" }
99
- }
100
- ]
101
- },
102
- created_by: "test",
103
- changelog: "Champion version"
104
- )
105
-
106
- @version2 = version_manager.save_version(
107
- rule_id: "test_rule",
108
- rule_content: {
109
- version: "1.0",
110
- ruleset: "challenger",
111
- rules: [
112
- {
113
- id: "rule2",
114
- if: { field: "amount", op: "gte", value: 0 },
115
- then: { decision: "review", weight: 1.0, reason: "Challenger" }
116
- }
117
- ]
118
- },
119
- created_by: "test",
120
- changelog: "Challenger version"
121
- )
122
-
123
- # Create A/B test
124
- @test = ab_test_manager.create_test(
125
- name: "Test AB",
126
- champion_version_id: @version1[:id],
127
- challenger_version_id: @version2[:id],
128
- traffic_split: { champion: 50, challenger: 50 }
129
- )
130
-
131
- # Start the test
132
- ab_test_manager.start_test(@test.id) if @test.status != "running"
133
- end
134
-
135
- after do
136
- FileUtils.rm_rf("./tmp/test_versions")
137
- end
138
-
139
- it "caches agents by version_id" do
140
- ab_agent = DecisionAgent::ABTesting::ABTestingAgent.new(
141
- ab_test_manager: ab_test_manager,
142
- cache_agents: true
143
- )
144
-
145
- # Make multiple decisions
146
- 5.times do
147
- ab_agent.decide(context: { amount: 100 }, ab_test_id: @test.id, user_id: "user1")
148
- end
149
-
150
- # Check cache stats
151
- stats = ab_agent.cache_stats
152
- expect(stats[:cached_agents]).to be > 0
153
- expect(stats[:version_ids]).not_to be_empty
154
- end
155
-
156
- it "can disable caching" do
157
- ab_agent = DecisionAgent::ABTesting::ABTestingAgent.new(
158
- ab_test_manager: ab_test_manager,
159
- cache_agents: false
160
- )
161
-
162
- # Make multiple decisions
163
- 3.times do
164
- ab_agent.decide(context: { amount: 100 }, ab_test_id: @test.id, user_id: "user2")
165
- end
166
-
167
- # Cache should be empty
168
- stats = ab_agent.cache_stats
169
- expect(stats[:cached_agents]).to eq(0)
170
- end
171
-
172
- it "allows clearing the cache" do
173
- ab_agent = DecisionAgent::ABTesting::ABTestingAgent.new(
174
- ab_test_manager: ab_test_manager,
175
- cache_agents: true
176
- )
177
-
178
- # Build cache
179
- ab_agent.decide(context: { amount: 100 }, ab_test_id: @test.id, user_id: "user3")
180
- expect(ab_agent.cache_stats[:cached_agents]).to be > 0
181
-
182
- # Clear cache
183
- ab_agent.clear_agent_cache!
184
- expect(ab_agent.cache_stats[:cached_agents]).to eq(0)
185
- end
186
-
187
- it "is thread-safe with concurrent access" do
188
- ab_agent = DecisionAgent::ABTesting::ABTestingAgent.new(
189
- ab_test_manager: ab_test_manager,
190
- cache_agents: true
191
- )
192
-
193
- threads = 10.times.map do |i|
194
- Thread.new do
195
- ab_agent.decide(context: { amount: 100 }, ab_test_id: @test.id, user_id: "user#{i}")
196
- end
197
- end
198
-
199
- threads.each(&:join)
200
-
201
- # Should have cached agents without errors
202
- expect(ab_agent.cache_stats[:cached_agents]).to be > 0
203
- end
204
- end
205
-
206
- describe "ConditionEvaluator caching" do
207
- before do
208
- # Clear caches before each test
209
- DecisionAgent::Dsl::ConditionEvaluator.clear_caches!
210
- end
211
-
212
- describe "regex caching" do
213
- it "caches compiled regexes" do
214
- context = DecisionAgent::Context.new({ email: "test@example.com" })
215
-
216
- # First evaluation compiles regex
217
- result1 = DecisionAgent::Dsl::ConditionEvaluator.evaluate(
218
- { "field" => "email", "op" => "matches", "value" => ".*@example\\.com$" },
219
- context
220
- )
221
-
222
- # Check cache
223
- stats = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
224
- expect(stats[:regex_cache_size]).to eq(1)
225
-
226
- # Second evaluation uses cached regex
227
- result2 = DecisionAgent::Dsl::ConditionEvaluator.evaluate(
228
- { "field" => "email", "op" => "matches", "value" => ".*@example\\.com$" },
229
- context
230
- )
231
-
232
- expect(result1).to eq(result2)
233
- expect(stats[:regex_cache_size]).to eq(1) # Still 1
234
- end
235
-
236
- it "handles Regexp objects without caching" do
237
- context = DecisionAgent::Context.new({ email: "test@example.com" })
238
- regex = /.*@example\.com$/
239
-
240
- result = DecisionAgent::Dsl::ConditionEvaluator.evaluate(
241
- { "field" => "email", "op" => "matches", "value" => regex },
242
- context
243
- )
244
-
245
- expect(result).to be true
246
- end
247
- end
248
-
249
- describe "path caching" do
250
- it "caches split paths for nested field access" do
251
- context = DecisionAgent::Context.new({ user: { profile: { role: "admin" } } })
252
-
253
- # First access splits path
254
- value1 = DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(
255
- context.to_h,
256
- "user.profile.role"
257
- )
258
-
259
- # Check cache
260
- stats = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
261
- expect(stats[:path_cache_size]).to eq(1)
262
-
263
- # Second access uses cached path
264
- value2 = DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(
265
- context.to_h,
266
- "user.profile.role"
267
- )
268
-
269
- expect(value1).to eq(value2)
270
- expect(value1).to eq("admin")
271
- end
272
-
273
- it "caches multiple different paths" do
274
- context = DecisionAgent::Context.new({
275
- user: { name: "Alice", age: 30 },
276
- order: { total: 100 }
277
- })
278
-
279
- DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(context.to_h, "user.name")
280
- DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(context.to_h, "user.age")
281
- DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(context.to_h, "order.total")
282
-
283
- stats = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
284
- expect(stats[:path_cache_size]).to eq(3)
285
- end
286
- end
287
-
288
- describe "date caching" do
289
- it "caches parsed dates" do
290
- context = DecisionAgent::Context.new({ created_at: "2025-01-01T00:00:00Z" })
291
-
292
- # First evaluation parses date
293
- result1 = DecisionAgent::Dsl::ConditionEvaluator.evaluate(
294
- { "field" => "created_at", "op" => "after_date", "value" => "2024-12-01T00:00:00Z" },
295
- context
296
- )
297
-
298
- # Check cache
299
- stats = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
300
- expect(stats[:date_cache_size]).to be > 0
301
-
302
- # Second evaluation uses cached date
303
- result2 = DecisionAgent::Dsl::ConditionEvaluator.evaluate(
304
- { "field" => "created_at", "op" => "after_date", "value" => "2024-12-01T00:00:00Z" },
305
- context
306
- )
307
-
308
- expect(result1).to eq(result2)
309
- expect(result1).to be true
310
- end
311
-
312
- it "does not cache Time/Date objects" do
313
- context = DecisionAgent::Context.new({ created_at: Time.now })
314
-
315
- result = DecisionAgent::Dsl::ConditionEvaluator.evaluate(
316
- { "field" => "created_at", "op" => "after_date", "value" => Time.now - 3600 },
317
- context
318
- )
319
-
320
- expect(result).to be true
321
- end
322
- end
323
-
324
- describe "cache management" do
325
- it "can clear all caches" do
326
- context = DecisionAgent::Context.new({
327
- email: "test@example.com",
328
- user: { role: "admin" },
329
- created_at: "2025-01-01T00:00:00Z"
330
- })
331
-
332
- # Populate caches
333
- DecisionAgent::Dsl::ConditionEvaluator.evaluate(
334
- { "field" => "email", "op" => "matches", "value" => ".*@example\\.com$" },
335
- context
336
- )
337
- DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(context.to_h, "user.role")
338
- DecisionAgent::Dsl::ConditionEvaluator.evaluate(
339
- { "field" => "created_at", "op" => "after_date", "value" => "2024-01-01T00:00:00Z" },
340
- context
341
- )
342
-
343
- stats_before = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
344
- expect(stats_before.values.sum).to be > 0
345
-
346
- # Clear caches
347
- DecisionAgent::Dsl::ConditionEvaluator.clear_caches!
348
-
349
- stats_after = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
350
- expect(stats_after[:regex_cache_size]).to eq(0)
351
- expect(stats_after[:path_cache_size]).to eq(0)
352
- expect(stats_after[:date_cache_size]).to eq(0)
353
- end
354
- end
355
-
356
- describe "thread safety" do
357
- it "handles concurrent cache access safely" do
358
- context = DecisionAgent::Context.new({
359
- email: "test@example.com",
360
- user: { profile: { role: "admin" } }
361
- })
362
-
363
- threads = 20.times.map do |_i|
364
- Thread.new do
365
- # Regex caching
366
- DecisionAgent::Dsl::ConditionEvaluator.evaluate(
367
- { "field" => "email", "op" => "matches", "value" => ".*@example\\.com$" },
368
- context
369
- )
370
-
371
- # Path caching
372
- DecisionAgent::Dsl::ConditionEvaluator.get_nested_value(
373
- context.to_h,
374
- "user.profile.role"
375
- )
376
-
377
- # Date caching
378
- DecisionAgent::Dsl::ConditionEvaluator.evaluate(
379
- { "field" => "created_at", "op" => "after_date", "value" => "2024-01-01T00:00:00Z" },
380
- DecisionAgent::Context.new({ created_at: "2025-01-01T00:00:00Z" })
381
- )
382
- end
383
- end
384
-
385
- threads.each(&:join)
386
-
387
- # Caches should be populated without errors
388
- stats = DecisionAgent::Dsl::ConditionEvaluator.cache_stats
389
- expect(stats[:regex_cache_size]).to be > 0
390
- expect(stats[:path_cache_size]).to be > 0
391
- end
392
- end
393
- end
394
-
395
- describe "WebSocket broadcasting optimization" do
396
- it "skips broadcast when no clients are connected" do
397
- # This is tested indirectly through the dashboard server
398
- # The optimization is in the broadcast_to_clients method
399
- # which returns early if @websocket_clients.empty?
400
-
401
- # We can verify the optimization exists in the code
402
- server_code = File.read("lib/decision_agent/monitoring/dashboard_server.rb")
403
- expect(server_code).to include("return if @websocket_clients.empty?")
404
- end
405
- end
406
-
407
- describe "Performance benchmarks" do
408
- let(:evaluator) do
409
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(
410
- rules_json: {
411
- version: "1.0",
412
- ruleset: "benchmark",
413
- rules: [
414
- {
415
- id: "rule1",
416
- if: {
417
- all: [
418
- { field: "amount", op: "gte", value: 100 },
419
- { field: "user.verified", op: "eq", value: true },
420
- { field: "email", op: "matches", value: ".*@example\\.com$" }
421
- ]
422
- },
423
- then: { decision: "approve", weight: 1.0, reason: "Approved" }
424
- }
425
- ]
426
- }
427
- )
428
- end
429
- let(:agent) { DecisionAgent::Agent.new(evaluators: [evaluator], validate_evaluations: false) }
430
-
431
- it "maintains high throughput with optimizations" do
432
- require "benchmark"
433
-
434
- # Warm up JIT and caches
435
- 100.times { agent.decide(context: { amount: 150, user: { verified: true }, email: "test@example.com" }) }
436
-
437
- iterations = 2000
438
- context = { amount: 150, user: { verified: true }, email: "test@example.com" }
439
-
440
- time = Benchmark.realtime do
441
- iterations.times do
442
- agent.decide(context: context)
443
- end
444
- end
445
-
446
- throughput = iterations / time
447
- puts "\nThroughput: #{throughput.round(2)} decisions/second"
448
-
449
- # Should maintain at least 2000 decisions/second
450
- # Note: This test uses regex matching which is more expensive than simple comparisons.
451
- # The threshold accounts for system variability, complex rules, test environment, and
452
- # potential interference from other tests when running in the full suite.
453
- # For simpler rules in production, expect 5,000-8,000+ decisions/second (see PERFORMANCE_AND_THREAD_SAFETY.md)
454
- expect(throughput).to be > 2000
455
- end
456
-
457
- it "benefits from caching on repeated evaluations" do
458
- require "benchmark"
459
-
460
- iterations = 1000
461
- context = { amount: 150, user: { verified: true }, email: "test@example.com" }
462
-
463
- # Warm up caches
464
- 10.times { agent.decide(context: context) }
465
-
466
- # Measure with warm cache
467
- warm_time = Benchmark.realtime do
468
- iterations.times { agent.decide(context: context) }
469
- end
470
-
471
- # Clear caches
472
- DecisionAgent::Dsl::ConditionEvaluator.clear_caches!
473
-
474
- # Measure with cold cache
475
- cold_time = Benchmark.realtime do
476
- iterations.times { agent.decide(context: context) }
477
- end
478
-
479
- warm_throughput = iterations / warm_time
480
- cold_throughput = iterations / cold_time
481
-
482
- puts "\nWarm cache throughput: #{warm_throughput.round(2)} decisions/second"
483
- puts "Cold cache throughput: #{cold_throughput.round(2)} decisions/second"
484
- puts "Improvement: #{(((warm_throughput / cold_throughput) - 1) * 100).round(2)}%"
485
-
486
- # NOTE: Cache warming may not always show improvement in microbenchmarks
487
- # due to Ruby's JIT, GC, and other factors. The important thing is
488
- # that caching doesn't make things slower.
489
- expect(warm_throughput).to be > 0
490
- expect(cold_throughput).to be > 0
491
- end
492
- end
493
- end