decision_agent 0.1.3 → 0.1.6
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 +84 -233
- data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
- data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
- data/lib/decision_agent/agent.rb +5 -3
- data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
- data/lib/decision_agent/auth/authenticator.rb +127 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
- data/lib/decision_agent/auth/password_reset_token.rb +33 -0
- data/lib/decision_agent/auth/permission.rb +29 -0
- data/lib/decision_agent/auth/permission_checker.rb +43 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
- data/lib/decision_agent/auth/rbac_config.rb +51 -0
- data/lib/decision_agent/auth/role.rb +56 -0
- data/lib/decision_agent/auth/session.rb +33 -0
- data/lib/decision_agent/auth/session_manager.rb +57 -0
- data/lib/decision_agent/auth/user.rb +70 -0
- data/lib/decision_agent/context.rb +24 -4
- data/lib/decision_agent/decision.rb +10 -3
- data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
- data/lib/decision_agent/dsl/schema_validator.rb +8 -1
- data/lib/decision_agent/errors.rb +38 -0
- data/lib/decision_agent/evaluation.rb +10 -3
- data/lib/decision_agent/evaluation_validator.rb +8 -13
- data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
- data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
- data/lib/decision_agent/testing/test_scenario.rb +42 -0
- data/lib/decision_agent/version.rb +10 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
- data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
- data/lib/decision_agent/web/public/app.js +184 -29
- data/lib/decision_agent/web/public/batch_testing.html +640 -0
- data/lib/decision_agent/web/public/index.html +37 -9
- data/lib/decision_agent/web/public/login.html +298 -0
- data/lib/decision_agent/web/public/users.html +679 -0
- data/lib/decision_agent/web/server.rb +873 -7
- data/lib/decision_agent.rb +59 -0
- data/lib/generators/decision_agent/install/install_generator.rb +37 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
- data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
- data/spec/ab_testing/storage/adapter_spec.rb +64 -0
- data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
- data/spec/advanced_operators_spec.rb +1003 -0
- data/spec/agent_spec.rb +40 -0
- data/spec/audit_adapters_spec.rb +18 -0
- data/spec/auth/access_audit_logger_spec.rb +394 -0
- data/spec/auth/authenticator_spec.rb +112 -0
- data/spec/auth/password_reset_spec.rb +294 -0
- data/spec/auth/permission_checker_spec.rb +207 -0
- data/spec/auth/permission_spec.rb +73 -0
- data/spec/auth/rbac_adapter_spec.rb +550 -0
- data/spec/auth/rbac_config_spec.rb +82 -0
- data/spec/auth/role_spec.rb +51 -0
- data/spec/auth/session_manager_spec.rb +172 -0
- data/spec/auth/session_spec.rb +112 -0
- data/spec/auth/user_spec.rb +130 -0
- data/spec/context_spec.rb +43 -0
- data/spec/decision_agent_spec.rb +96 -0
- data/spec/decision_spec.rb +423 -0
- data/spec/dsl/condition_evaluator_spec.rb +774 -0
- data/spec/evaluation_spec.rb +364 -0
- data/spec/evaluation_validator_spec.rb +165 -0
- data/spec/examples.txt +1542 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +221 -3
- data/spec/monitoring/monitored_agent_spec.rb +1 -1
- data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- data/spec/performance_optimizations_spec.rb +486 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/testing/batch_test_importer_spec.rb +693 -0
- data/spec/testing/batch_test_runner_spec.rb +307 -0
- data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
- data/spec/testing/test_result_comparator_spec.rb +392 -0
- data/spec/testing/test_scenario_spec.rb +113 -0
- data/spec/versioning/adapter_spec.rb +156 -0
- data/spec/versioning_spec.rb +253 -0
- data/spec/web/middleware/auth_middleware_spec.rb +133 -0
- data/spec/web/middleware/permission_middleware_spec.rb +247 -0
- data/spec/web_ui_rack_spec.rb +1705 -0
- metadata +123 -6
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe DecisionAgent::Evaluation do
|
|
4
|
+
describe "#initialize" do
|
|
5
|
+
it "creates an evaluation with all required fields" do
|
|
6
|
+
evaluation = described_class.new(
|
|
7
|
+
decision: "approve",
|
|
8
|
+
weight: 0.8,
|
|
9
|
+
reason: "Test reason",
|
|
10
|
+
evaluator_name: "TestEvaluator"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
expect(evaluation.decision).to eq("approve")
|
|
14
|
+
expect(evaluation.weight).to eq(0.8)
|
|
15
|
+
expect(evaluation.reason).to eq("Test reason")
|
|
16
|
+
expect(evaluation.evaluator_name).to eq("TestEvaluator")
|
|
17
|
+
expect(evaluation.metadata).to eq({})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "converts decision to string" do
|
|
21
|
+
evaluation = described_class.new(
|
|
22
|
+
decision: :approve,
|
|
23
|
+
weight: 0.8,
|
|
24
|
+
reason: "Test",
|
|
25
|
+
evaluator_name: "Test"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(evaluation.decision).to eq("approve")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "converts weight to float" do
|
|
32
|
+
evaluation = described_class.new(
|
|
33
|
+
decision: "approve",
|
|
34
|
+
weight: "0.8",
|
|
35
|
+
reason: "Test",
|
|
36
|
+
evaluator_name: "Test"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(evaluation.weight).to eq(0.8)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "converts reason to string" do
|
|
43
|
+
evaluation = described_class.new(
|
|
44
|
+
decision: "approve",
|
|
45
|
+
weight: 0.8,
|
|
46
|
+
reason: :test_reason,
|
|
47
|
+
evaluator_name: "Test"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
expect(evaluation.reason).to eq("test_reason")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "converts evaluator_name to string" do
|
|
54
|
+
evaluation = described_class.new(
|
|
55
|
+
decision: "approve",
|
|
56
|
+
weight: 0.8,
|
|
57
|
+
reason: "Test",
|
|
58
|
+
evaluator_name: :TestEvaluator
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(evaluation.evaluator_name).to eq("TestEvaluator")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "freezes the evaluation object" do
|
|
65
|
+
evaluation = described_class.new(
|
|
66
|
+
decision: "approve",
|
|
67
|
+
weight: 0.8,
|
|
68
|
+
reason: "Test",
|
|
69
|
+
evaluator_name: "Test"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expect(evaluation).to be_frozen
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "freezes nested structures" do
|
|
76
|
+
evaluation = described_class.new(
|
|
77
|
+
decision: "approve",
|
|
78
|
+
weight: 0.8,
|
|
79
|
+
reason: "Test",
|
|
80
|
+
evaluator_name: "Test",
|
|
81
|
+
metadata: { key: "value", nested: { data: [1, 2, 3] } }
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(evaluation.decision).to be_frozen
|
|
85
|
+
expect(evaluation.reason).to be_frozen
|
|
86
|
+
expect(evaluation.evaluator_name).to be_frozen
|
|
87
|
+
expect(evaluation.metadata).to be_frozen
|
|
88
|
+
expect(evaluation.metadata[:nested]).to be_frozen
|
|
89
|
+
expect(evaluation.metadata[:nested][:data]).to be_frozen
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "freezes metadata in-place without creating new objects" do
|
|
93
|
+
original_metadata = { key: "value", nested: { data: [1, 2, 3] } }
|
|
94
|
+
original_metadata_id = original_metadata.object_id
|
|
95
|
+
original_nested_id = original_metadata[:nested].object_id
|
|
96
|
+
|
|
97
|
+
evaluation = described_class.new(
|
|
98
|
+
decision: "approve",
|
|
99
|
+
weight: 0.8,
|
|
100
|
+
reason: "Test",
|
|
101
|
+
evaluator_name: "Test",
|
|
102
|
+
metadata: original_metadata
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Should freeze in-place, not create new objects
|
|
106
|
+
expect(evaluation.metadata.object_id).to eq(original_metadata_id)
|
|
107
|
+
expect(evaluation.metadata[:nested].object_id).to eq(original_nested_id)
|
|
108
|
+
expect(evaluation.metadata).to be_frozen
|
|
109
|
+
expect(evaluation.metadata[:nested]).to be_frozen
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "skips already frozen objects in deep_freeze" do
|
|
113
|
+
frozen_metadata = { key: "value", nested: { data: [1, 2, 3] } }
|
|
114
|
+
frozen_metadata.freeze
|
|
115
|
+
frozen_metadata[:nested].freeze
|
|
116
|
+
|
|
117
|
+
evaluation = described_class.new(
|
|
118
|
+
decision: "approve",
|
|
119
|
+
weight: 0.8,
|
|
120
|
+
reason: "Test",
|
|
121
|
+
evaluator_name: "Test",
|
|
122
|
+
metadata: frozen_metadata
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
expect(evaluation.metadata).to be_frozen
|
|
126
|
+
expect(evaluation.metadata[:nested]).to be_frozen
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "does not freeze hash keys unnecessarily" do
|
|
130
|
+
key_symbol = :test_key
|
|
131
|
+
key_string = "test_key"
|
|
132
|
+
metadata = {
|
|
133
|
+
key_symbol => "value1",
|
|
134
|
+
key_string => "value2"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
evaluation = described_class.new(
|
|
138
|
+
decision: "approve",
|
|
139
|
+
weight: 0.8,
|
|
140
|
+
reason: "Test",
|
|
141
|
+
evaluator_name: "Test",
|
|
142
|
+
metadata: metadata
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Keys should not be frozen (they're typically symbols/strings that don't need freezing)
|
|
146
|
+
expect(evaluation.metadata.keys.first).to eq(key_symbol)
|
|
147
|
+
expect(evaluation.metadata.keys.last).to eq(key_string)
|
|
148
|
+
# Values should be frozen
|
|
149
|
+
expect(evaluation.metadata[key_symbol]).to be_frozen
|
|
150
|
+
expect(evaluation.metadata[key_string]).to be_frozen
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "raises error for weight outside 0-1 range" do
|
|
154
|
+
expect do
|
|
155
|
+
described_class.new(
|
|
156
|
+
decision: "approve",
|
|
157
|
+
weight: 1.5,
|
|
158
|
+
reason: "Test",
|
|
159
|
+
evaluator_name: "Test"
|
|
160
|
+
)
|
|
161
|
+
end.to raise_error(DecisionAgent::InvalidWeightError)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "raises error for negative weight" do
|
|
165
|
+
expect do
|
|
166
|
+
described_class.new(
|
|
167
|
+
decision: "approve",
|
|
168
|
+
weight: -0.1,
|
|
169
|
+
reason: "Test",
|
|
170
|
+
evaluator_name: "Test"
|
|
171
|
+
)
|
|
172
|
+
end.to raise_error(DecisionAgent::InvalidWeightError)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "accepts weight at boundaries" do
|
|
176
|
+
eval1 = described_class.new(
|
|
177
|
+
decision: "approve",
|
|
178
|
+
weight: 0.0,
|
|
179
|
+
reason: "Test",
|
|
180
|
+
evaluator_name: "Test"
|
|
181
|
+
)
|
|
182
|
+
expect(eval1.weight).to eq(0.0)
|
|
183
|
+
|
|
184
|
+
eval2 = described_class.new(
|
|
185
|
+
decision: "approve",
|
|
186
|
+
weight: 1.0,
|
|
187
|
+
reason: "Test",
|
|
188
|
+
evaluator_name: "Test"
|
|
189
|
+
)
|
|
190
|
+
expect(eval2.weight).to eq(1.0)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "handles metadata" do
|
|
194
|
+
metadata = { rule_id: "rule_1", source: "test" }
|
|
195
|
+
evaluation = described_class.new(
|
|
196
|
+
decision: "approve",
|
|
197
|
+
weight: 0.8,
|
|
198
|
+
reason: "Test",
|
|
199
|
+
evaluator_name: "Test",
|
|
200
|
+
metadata: metadata
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
expect(evaluation.metadata).to eq(metadata)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "defaults to empty metadata" do
|
|
207
|
+
evaluation = described_class.new(
|
|
208
|
+
decision: "approve",
|
|
209
|
+
weight: 0.8,
|
|
210
|
+
reason: "Test",
|
|
211
|
+
evaluator_name: "Test"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
expect(evaluation.metadata).to eq({})
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
describe "#to_h" do
|
|
219
|
+
it "converts evaluation to hash" do
|
|
220
|
+
evaluation = described_class.new(
|
|
221
|
+
decision: "approve",
|
|
222
|
+
weight: 0.8,
|
|
223
|
+
reason: "Test reason",
|
|
224
|
+
evaluator_name: "TestEvaluator",
|
|
225
|
+
metadata: { key: "value" }
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
hash = evaluation.to_h
|
|
229
|
+
|
|
230
|
+
expect(hash).to be_a(Hash)
|
|
231
|
+
expect(hash[:decision]).to eq("approve")
|
|
232
|
+
expect(hash[:weight]).to eq(0.8)
|
|
233
|
+
expect(hash[:reason]).to eq("Test reason")
|
|
234
|
+
expect(hash[:evaluator_name]).to eq("TestEvaluator")
|
|
235
|
+
expect(hash[:metadata]).to eq({ key: "value" })
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
describe "#==" do
|
|
240
|
+
it "compares evaluations by all fields" do
|
|
241
|
+
eval1 = described_class.new(
|
|
242
|
+
decision: "approve",
|
|
243
|
+
weight: 0.8,
|
|
244
|
+
reason: "Test",
|
|
245
|
+
evaluator_name: "Test",
|
|
246
|
+
metadata: { key: "value" }
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
eval2 = described_class.new(
|
|
250
|
+
decision: "approve",
|
|
251
|
+
weight: 0.8,
|
|
252
|
+
reason: "Test",
|
|
253
|
+
evaluator_name: "Test",
|
|
254
|
+
metadata: { key: "value" }
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
expect(eval1).to eq(eval2)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "returns false for different decisions" do
|
|
261
|
+
eval1 = described_class.new(
|
|
262
|
+
decision: "approve",
|
|
263
|
+
weight: 0.8,
|
|
264
|
+
reason: "Test",
|
|
265
|
+
evaluator_name: "Test"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
eval2 = described_class.new(
|
|
269
|
+
decision: "reject",
|
|
270
|
+
weight: 0.8,
|
|
271
|
+
reason: "Test",
|
|
272
|
+
evaluator_name: "Test"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
expect(eval1).not_to eq(eval2)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "returns false for different weights" do
|
|
279
|
+
eval1 = described_class.new(
|
|
280
|
+
decision: "approve",
|
|
281
|
+
weight: 0.8,
|
|
282
|
+
reason: "Test",
|
|
283
|
+
evaluator_name: "Test"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
eval2 = described_class.new(
|
|
287
|
+
decision: "approve",
|
|
288
|
+
weight: 0.9,
|
|
289
|
+
reason: "Test",
|
|
290
|
+
evaluator_name: "Test"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
expect(eval1).not_to eq(eval2)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it "returns false for different reasons" do
|
|
297
|
+
eval1 = described_class.new(
|
|
298
|
+
decision: "approve",
|
|
299
|
+
weight: 0.8,
|
|
300
|
+
reason: "Reason 1",
|
|
301
|
+
evaluator_name: "Test"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
eval2 = described_class.new(
|
|
305
|
+
decision: "approve",
|
|
306
|
+
weight: 0.8,
|
|
307
|
+
reason: "Reason 2",
|
|
308
|
+
evaluator_name: "Test"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
expect(eval1).not_to eq(eval2)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "returns false for different evaluator names" do
|
|
315
|
+
eval1 = described_class.new(
|
|
316
|
+
decision: "approve",
|
|
317
|
+
weight: 0.8,
|
|
318
|
+
reason: "Test",
|
|
319
|
+
evaluator_name: "Evaluator1"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
eval2 = described_class.new(
|
|
323
|
+
decision: "approve",
|
|
324
|
+
weight: 0.8,
|
|
325
|
+
reason: "Test",
|
|
326
|
+
evaluator_name: "Evaluator2"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
expect(eval1).not_to eq(eval2)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it "returns false for different metadata" do
|
|
333
|
+
eval1 = described_class.new(
|
|
334
|
+
decision: "approve",
|
|
335
|
+
weight: 0.8,
|
|
336
|
+
reason: "Test",
|
|
337
|
+
evaluator_name: "Test",
|
|
338
|
+
metadata: { key: "value1" }
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
eval2 = described_class.new(
|
|
342
|
+
decision: "approve",
|
|
343
|
+
weight: 0.8,
|
|
344
|
+
reason: "Test",
|
|
345
|
+
evaluator_name: "Test",
|
|
346
|
+
metadata: { key: "value2" }
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
expect(eval1).not_to eq(eval2)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it "returns false for non-Evaluation objects" do
|
|
353
|
+
evaluation = described_class.new(
|
|
354
|
+
decision: "approve",
|
|
355
|
+
weight: 0.8,
|
|
356
|
+
reason: "Test",
|
|
357
|
+
evaluator_name: "Test"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
expect(evaluation).not_to eq("not an evaluation")
|
|
361
|
+
expect(evaluation).not_to eq(nil)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe DecisionAgent::EvaluationValidator do
|
|
4
|
+
let(:valid_evaluation) do
|
|
5
|
+
DecisionAgent::Evaluation.new(
|
|
6
|
+
decision: "approve",
|
|
7
|
+
weight: 0.8,
|
|
8
|
+
reason: "Valid reason",
|
|
9
|
+
evaluator_name: "TestEvaluator"
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe ".validate!" do
|
|
14
|
+
it "validates a valid evaluation" do
|
|
15
|
+
expect do
|
|
16
|
+
described_class.validate!(valid_evaluation)
|
|
17
|
+
end.not_to raise_error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "raises error for nil evaluation" do
|
|
21
|
+
expect do
|
|
22
|
+
described_class.validate!(nil)
|
|
23
|
+
end.to raise_error(described_class::ValidationError, /cannot be nil/)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "raises error for non-Evaluation object" do
|
|
27
|
+
expect do
|
|
28
|
+
described_class.validate!("not an evaluation")
|
|
29
|
+
end.to raise_error(described_class::ValidationError, /must be an Evaluation instance/)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "validates multiple valid evaluations" do
|
|
33
|
+
eval1 = DecisionAgent::Evaluation.new(
|
|
34
|
+
decision: "approve",
|
|
35
|
+
weight: 0.8,
|
|
36
|
+
reason: "Reason 1",
|
|
37
|
+
evaluator_name: "Eval1"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
eval2 = DecisionAgent::Evaluation.new(
|
|
41
|
+
decision: "reject",
|
|
42
|
+
weight: 0.9,
|
|
43
|
+
reason: "Reason 2",
|
|
44
|
+
evaluator_name: "Eval2"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
expect do
|
|
48
|
+
described_class.validate!(eval1)
|
|
49
|
+
described_class.validate!(eval2)
|
|
50
|
+
end.not_to raise_error
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe ".validate_all!" do
|
|
55
|
+
it "validates an array of valid evaluations" do
|
|
56
|
+
evaluations = [
|
|
57
|
+
valid_evaluation,
|
|
58
|
+
DecisionAgent::Evaluation.new(
|
|
59
|
+
decision: "reject",
|
|
60
|
+
weight: 0.9,
|
|
61
|
+
reason: "Another reason",
|
|
62
|
+
evaluator_name: "OtherEvaluator"
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
expect do
|
|
67
|
+
described_class.validate_all!(evaluations)
|
|
68
|
+
end.not_to raise_error
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "raises error for non-array input" do
|
|
72
|
+
expect do
|
|
73
|
+
described_class.validate_all!("not an array")
|
|
74
|
+
end.to raise_error(described_class::ValidationError, /must be an Array/)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "raises error for empty array" do
|
|
78
|
+
expect do
|
|
79
|
+
described_class.validate_all!([])
|
|
80
|
+
end.to raise_error(described_class::ValidationError, /cannot be empty/)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "validates all evaluations in array" do
|
|
84
|
+
eval1 = DecisionAgent::Evaluation.new(
|
|
85
|
+
decision: "approve",
|
|
86
|
+
weight: 0.8,
|
|
87
|
+
reason: "Reason 1",
|
|
88
|
+
evaluator_name: "Eval1"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
eval2 = DecisionAgent::Evaluation.new(
|
|
92
|
+
decision: "reject",
|
|
93
|
+
weight: 0.9,
|
|
94
|
+
reason: "Reason 2",
|
|
95
|
+
evaluator_name: "Eval2"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
expect do
|
|
99
|
+
described_class.validate_all!([eval1, eval2])
|
|
100
|
+
end.not_to raise_error
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "includes index in error message for invalid evaluation" do
|
|
104
|
+
evaluations = [
|
|
105
|
+
valid_evaluation,
|
|
106
|
+
nil # Invalid evaluation
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
expect do
|
|
110
|
+
described_class.validate_all!(evaluations)
|
|
111
|
+
end.to raise_error(described_class::ValidationError, /index 1/)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "optimized frozen validation" do
|
|
116
|
+
it "uses fast path for frozen evaluations" do
|
|
117
|
+
# Evaluations are always frozen in their initializer
|
|
118
|
+
evaluation = DecisionAgent::Evaluation.new(
|
|
119
|
+
decision: "approve",
|
|
120
|
+
weight: 0.8,
|
|
121
|
+
reason: "Test reason",
|
|
122
|
+
evaluator_name: "TestEvaluator"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
expect(evaluation).to be_frozen
|
|
126
|
+
expect do
|
|
127
|
+
described_class.validate!(evaluation)
|
|
128
|
+
end.not_to raise_error
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "skips nested frozen checks when evaluation is frozen" do
|
|
132
|
+
# Since evaluations are always frozen in initializer,
|
|
133
|
+
# the optimized validator should skip checking nested structures
|
|
134
|
+
evaluation = DecisionAgent::Evaluation.new(
|
|
135
|
+
decision: "approve",
|
|
136
|
+
weight: 0.8,
|
|
137
|
+
reason: "Test reason",
|
|
138
|
+
evaluator_name: "TestEvaluator",
|
|
139
|
+
metadata: { nested: { data: "value" } }
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(evaluation).to be_frozen
|
|
143
|
+
expect(evaluation.metadata).to be_frozen
|
|
144
|
+
expect do
|
|
145
|
+
described_class.validate!(evaluation)
|
|
146
|
+
end.not_to raise_error
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "still validates unfrozen evaluations" do
|
|
150
|
+
# Create a mock object that isn't frozen (simulating an edge case)
|
|
151
|
+
# In practice, evaluations are always frozen in their initializer
|
|
152
|
+
unfrozen_evaluation = double("UnfrozenEvaluation")
|
|
153
|
+
allow(unfrozen_evaluation).to receive(:frozen?).and_return(false)
|
|
154
|
+
allow(unfrozen_evaluation).to receive(:is_a?).with(DecisionAgent::Evaluation).and_return(true)
|
|
155
|
+
allow(unfrozen_evaluation).to receive(:decision).and_return("approve")
|
|
156
|
+
allow(unfrozen_evaluation).to receive(:weight).and_return(0.8)
|
|
157
|
+
allow(unfrozen_evaluation).to receive(:reason).and_return("Test reason")
|
|
158
|
+
allow(unfrozen_evaluation).to receive(:evaluator_name).and_return("TestEvaluator")
|
|
159
|
+
|
|
160
|
+
expect do
|
|
161
|
+
described_class.validate!(unfrozen_evaluation)
|
|
162
|
+
end.to raise_error(described_class::ValidationError, /must be frozen/)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|