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.
- checksums.yaml +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
data/spec/thread_safety_spec.rb
DELETED
|
@@ -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
|