decision_agent 0.1.4 → 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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
  4. data/lib/decision_agent/agent.rb +5 -3
  5. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  6. data/lib/decision_agent/auth/authenticator.rb +127 -0
  7. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  8. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  9. data/lib/decision_agent/auth/permission.rb +29 -0
  10. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  11. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  12. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  13. data/lib/decision_agent/auth/role.rb +56 -0
  14. data/lib/decision_agent/auth/session.rb +33 -0
  15. data/lib/decision_agent/auth/session_manager.rb +57 -0
  16. data/lib/decision_agent/auth/user.rb +70 -0
  17. data/lib/decision_agent/context.rb +24 -4
  18. data/lib/decision_agent/decision.rb +10 -3
  19. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  20. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  21. data/lib/decision_agent/errors.rb +38 -0
  22. data/lib/decision_agent/evaluation.rb +10 -3
  23. data/lib/decision_agent/evaluation_validator.rb +8 -13
  24. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  25. data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
  26. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  27. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  28. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  29. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  30. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  31. data/lib/decision_agent/version.rb +10 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  33. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  34. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  35. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  36. data/lib/decision_agent/web/public/app.js +184 -29
  37. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  38. data/lib/decision_agent/web/public/index.html +37 -9
  39. data/lib/decision_agent/web/public/login.html +298 -0
  40. data/lib/decision_agent/web/public/users.html +679 -0
  41. data/lib/decision_agent/web/server.rb +873 -7
  42. data/lib/decision_agent.rb +52 -0
  43. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  45. data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
  46. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  47. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  48. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  49. data/spec/advanced_operators_spec.rb +1003 -0
  50. data/spec/agent_spec.rb +40 -0
  51. data/spec/audit_adapters_spec.rb +18 -0
  52. data/spec/auth/access_audit_logger_spec.rb +394 -0
  53. data/spec/auth/authenticator_spec.rb +112 -0
  54. data/spec/auth/password_reset_spec.rb +294 -0
  55. data/spec/auth/permission_checker_spec.rb +207 -0
  56. data/spec/auth/permission_spec.rb +73 -0
  57. data/spec/auth/rbac_adapter_spec.rb +550 -0
  58. data/spec/auth/rbac_config_spec.rb +82 -0
  59. data/spec/auth/role_spec.rb +51 -0
  60. data/spec/auth/session_manager_spec.rb +172 -0
  61. data/spec/auth/session_spec.rb +112 -0
  62. data/spec/auth/user_spec.rb +130 -0
  63. data/spec/context_spec.rb +43 -0
  64. data/spec/decision_agent_spec.rb +96 -0
  65. data/spec/decision_spec.rb +423 -0
  66. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  67. data/spec/evaluation_spec.rb +364 -0
  68. data/spec/evaluation_validator_spec.rb +165 -0
  69. data/spec/examples.txt +1542 -612
  70. data/spec/monitoring/metrics_collector_spec.rb +220 -2
  71. data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
  72. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  73. data/spec/performance_optimizations_spec.rb +486 -0
  74. data/spec/spec_helper.rb +23 -0
  75. data/spec/testing/batch_test_importer_spec.rb +693 -0
  76. data/spec/testing/batch_test_runner_spec.rb +307 -0
  77. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  78. data/spec/testing/test_result_comparator_spec.rb +392 -0
  79. data/spec/testing/test_scenario_spec.rb +113 -0
  80. data/spec/versioning/adapter_spec.rb +156 -0
  81. data/spec/versioning_spec.rb +253 -0
  82. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  83. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  84. data/spec/web_ui_rack_spec.rb +1705 -0
  85. metadata +99 -6
@@ -0,0 +1,423 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent::Decision do
4
+ let(:evaluation) do
5
+ DecisionAgent::Evaluation.new(
6
+ decision: "approve",
7
+ weight: 0.8,
8
+ reason: "Test reason",
9
+ evaluator_name: "TestEvaluator"
10
+ )
11
+ end
12
+
13
+ let(:audit_payload) do
14
+ {
15
+ timestamp: "2025-01-01T00:00:00Z",
16
+ context: { user: "test" },
17
+ decision: "approve",
18
+ confidence: 0.8
19
+ }
20
+ end
21
+
22
+ describe "#initialize" do
23
+ it "creates a decision with all required fields" do
24
+ decision = described_class.new(
25
+ decision: "approve",
26
+ confidence: 0.8,
27
+ explanations: ["Test explanation"],
28
+ evaluations: [evaluation],
29
+ audit_payload: audit_payload
30
+ )
31
+
32
+ expect(decision.decision).to eq("approve")
33
+ expect(decision.confidence).to eq(0.8)
34
+ expect(decision.explanations).to eq(["Test explanation"])
35
+ expect(decision.evaluations).to eq([evaluation])
36
+ expect(decision.audit_payload).to eq(audit_payload)
37
+ end
38
+
39
+ it "converts decision to string" do
40
+ decision = described_class.new(
41
+ decision: :approve,
42
+ confidence: 0.8,
43
+ explanations: [],
44
+ evaluations: [evaluation],
45
+ audit_payload: audit_payload
46
+ )
47
+
48
+ expect(decision.decision).to eq("approve")
49
+ end
50
+
51
+ it "converts confidence to float" do
52
+ decision = described_class.new(
53
+ decision: "approve",
54
+ confidence: "0.8",
55
+ explanations: [],
56
+ evaluations: [evaluation],
57
+ audit_payload: audit_payload
58
+ )
59
+
60
+ expect(decision.confidence).to eq(0.8)
61
+ end
62
+
63
+ it "freezes the decision object" do
64
+ decision = described_class.new(
65
+ decision: "approve",
66
+ confidence: 0.8,
67
+ explanations: [],
68
+ evaluations: [evaluation],
69
+ audit_payload: audit_payload
70
+ )
71
+
72
+ expect(decision).to be_frozen
73
+ end
74
+
75
+ it "freezes nested structures" do
76
+ decision = described_class.new(
77
+ decision: "approve",
78
+ confidence: 0.8,
79
+ explanations: ["explanation"],
80
+ evaluations: [evaluation],
81
+ audit_payload: audit_payload
82
+ )
83
+
84
+ expect(decision.decision).to be_frozen
85
+ expect(decision.explanations).to be_frozen
86
+ expect(decision.explanations.first).to be_frozen
87
+ expect(decision.evaluations).to be_frozen
88
+ end
89
+
90
+ it "deep freezes audit payload" do
91
+ nested_payload = {
92
+ context: { user: { name: "test" } },
93
+ metadata: [1, 2, 3]
94
+ }
95
+
96
+ decision = described_class.new(
97
+ decision: "approve",
98
+ confidence: 0.8,
99
+ explanations: [],
100
+ evaluations: [evaluation],
101
+ audit_payload: nested_payload
102
+ )
103
+
104
+ expect(decision.audit_payload).to be_frozen
105
+ expect(decision.audit_payload[:context]).to be_frozen
106
+ expect(decision.audit_payload[:context][:user]).to be_frozen
107
+ expect(decision.audit_payload[:metadata]).to be_frozen
108
+ end
109
+
110
+ it "freezes audit payload in-place without creating new objects" do
111
+ original_payload = {
112
+ context: { user: "test" },
113
+ metadata: [1, 2, 3]
114
+ }
115
+ original_payload_id = original_payload.object_id
116
+ original_context_id = original_payload[:context].object_id
117
+
118
+ decision = described_class.new(
119
+ decision: "approve",
120
+ confidence: 0.8,
121
+ explanations: [],
122
+ evaluations: [evaluation],
123
+ audit_payload: original_payload
124
+ )
125
+
126
+ # Should freeze in-place, not create new objects
127
+ expect(decision.audit_payload.object_id).to eq(original_payload_id)
128
+ expect(decision.audit_payload[:context].object_id).to eq(original_context_id)
129
+ expect(decision.audit_payload).to be_frozen
130
+ expect(decision.audit_payload[:context]).to be_frozen
131
+ end
132
+
133
+ it "skips already frozen objects in deep_freeze" do
134
+ frozen_payload = {
135
+ context: { user: "test" }
136
+ }
137
+ frozen_payload.freeze
138
+ frozen_payload[:context].freeze
139
+
140
+ decision = described_class.new(
141
+ decision: "approve",
142
+ confidence: 0.8,
143
+ explanations: [],
144
+ evaluations: [evaluation],
145
+ audit_payload: frozen_payload
146
+ )
147
+
148
+ expect(decision.audit_payload).to be_frozen
149
+ expect(decision.audit_payload[:context]).to be_frozen
150
+ end
151
+
152
+ it "does not freeze hash keys unnecessarily" do
153
+ key_symbol = :test_key
154
+ key_string = "test_key"
155
+ payload = {
156
+ key_symbol => "value1",
157
+ key_string => "value2"
158
+ }
159
+
160
+ decision = described_class.new(
161
+ decision: "approve",
162
+ confidence: 0.8,
163
+ explanations: [],
164
+ evaluations: [evaluation],
165
+ audit_payload: payload
166
+ )
167
+
168
+ # Keys should not be frozen (they're typically symbols/strings that don't need freezing)
169
+ expect(decision.audit_payload.keys.first).to eq(key_symbol)
170
+ expect(decision.audit_payload.keys.last).to eq(key_string)
171
+ # Values should be frozen
172
+ expect(decision.audit_payload[key_symbol]).to be_frozen
173
+ expect(decision.audit_payload[key_string]).to be_frozen
174
+ end
175
+
176
+ it "raises error for confidence outside 0-1 range" do
177
+ expect do
178
+ described_class.new(
179
+ decision: "approve",
180
+ confidence: 1.5,
181
+ explanations: [],
182
+ evaluations: [evaluation],
183
+ audit_payload: audit_payload
184
+ )
185
+ end.to raise_error(DecisionAgent::InvalidConfidenceError)
186
+ end
187
+
188
+ it "raises error for negative confidence" do
189
+ expect do
190
+ described_class.new(
191
+ decision: "approve",
192
+ confidence: -0.1,
193
+ explanations: [],
194
+ evaluations: [evaluation],
195
+ audit_payload: audit_payload
196
+ )
197
+ end.to raise_error(DecisionAgent::InvalidConfidenceError)
198
+ end
199
+
200
+ it "accepts confidence at boundaries" do
201
+ decision1 = described_class.new(
202
+ decision: "approve",
203
+ confidence: 0.0,
204
+ explanations: [],
205
+ evaluations: [evaluation],
206
+ audit_payload: audit_payload
207
+ )
208
+ expect(decision1.confidence).to eq(0.0)
209
+
210
+ decision2 = described_class.new(
211
+ decision: "approve",
212
+ confidence: 1.0,
213
+ explanations: [],
214
+ evaluations: [evaluation],
215
+ audit_payload: audit_payload
216
+ )
217
+ expect(decision2.confidence).to eq(1.0)
218
+ end
219
+
220
+ it "handles array explanations" do
221
+ explanations = %w[explanation1 explanation2]
222
+ decision = described_class.new(
223
+ decision: "approve",
224
+ confidence: 0.8,
225
+ explanations: explanations,
226
+ evaluations: [evaluation],
227
+ audit_payload: audit_payload
228
+ )
229
+
230
+ expect(decision.explanations).to eq(explanations)
231
+ end
232
+
233
+ it "converts non-array explanations to array" do
234
+ decision = described_class.new(
235
+ decision: "approve",
236
+ confidence: 0.8,
237
+ explanations: "single explanation",
238
+ evaluations: [evaluation],
239
+ audit_payload: audit_payload
240
+ )
241
+
242
+ expect(decision.explanations).to eq(["single explanation"])
243
+ end
244
+ end
245
+
246
+ describe "#to_h" do
247
+ it "converts decision to hash" do
248
+ decision = described_class.new(
249
+ decision: "approve",
250
+ confidence: 0.8,
251
+ explanations: ["explanation"],
252
+ evaluations: [evaluation],
253
+ audit_payload: audit_payload
254
+ )
255
+
256
+ hash = decision.to_h
257
+
258
+ expect(hash).to be_a(Hash)
259
+ expect(hash[:decision]).to eq("approve")
260
+ expect(hash[:confidence]).to eq(0.8)
261
+ expect(hash[:explanations]).to eq(["explanation"])
262
+ expect(hash[:evaluations]).to be_an(Array)
263
+ expect(hash[:evaluations].first).to be_a(Hash)
264
+ expect(hash[:audit_payload]).to eq(audit_payload)
265
+ end
266
+
267
+ it "converts evaluations to hashes" do
268
+ decision = described_class.new(
269
+ decision: "approve",
270
+ confidence: 0.8,
271
+ explanations: [],
272
+ evaluations: [evaluation],
273
+ audit_payload: audit_payload
274
+ )
275
+
276
+ hash = decision.to_h
277
+ expect(hash[:evaluations].first[:decision]).to eq("approve")
278
+ expect(hash[:evaluations].first[:weight]).to eq(0.8)
279
+ end
280
+ end
281
+
282
+ describe "#==" do
283
+ it "compares decisions by all fields" do
284
+ decision1 = described_class.new(
285
+ decision: "approve",
286
+ confidence: 0.8,
287
+ explanations: ["explanation"],
288
+ evaluations: [evaluation],
289
+ audit_payload: audit_payload
290
+ )
291
+
292
+ decision2 = described_class.new(
293
+ decision: "approve",
294
+ confidence: 0.8,
295
+ explanations: ["explanation"],
296
+ evaluations: [evaluation],
297
+ audit_payload: audit_payload
298
+ )
299
+
300
+ expect(decision1).to eq(decision2)
301
+ end
302
+
303
+ it "returns false for different decisions" do
304
+ decision1 = described_class.new(
305
+ decision: "approve",
306
+ confidence: 0.8,
307
+ explanations: [],
308
+ evaluations: [evaluation],
309
+ audit_payload: audit_payload
310
+ )
311
+
312
+ decision2 = described_class.new(
313
+ decision: "reject",
314
+ confidence: 0.8,
315
+ explanations: [],
316
+ evaluations: [evaluation],
317
+ audit_payload: audit_payload
318
+ )
319
+
320
+ expect(decision1).not_to eq(decision2)
321
+ end
322
+
323
+ it "returns false for different confidences" do
324
+ decision1 = described_class.new(
325
+ decision: "approve",
326
+ confidence: 0.8,
327
+ explanations: [],
328
+ evaluations: [evaluation],
329
+ audit_payload: audit_payload
330
+ )
331
+
332
+ decision2 = described_class.new(
333
+ decision: "approve",
334
+ confidence: 0.9,
335
+ explanations: [],
336
+ evaluations: [evaluation],
337
+ audit_payload: audit_payload
338
+ )
339
+
340
+ expect(decision1).not_to eq(decision2)
341
+ end
342
+
343
+ it "allows small confidence differences" do
344
+ decision1 = described_class.new(
345
+ decision: "approve",
346
+ confidence: 0.8,
347
+ explanations: [],
348
+ evaluations: [evaluation],
349
+ audit_payload: audit_payload
350
+ )
351
+
352
+ decision2 = described_class.new(
353
+ decision: "approve",
354
+ confidence: 0.8000001,
355
+ explanations: [],
356
+ evaluations: [evaluation],
357
+ audit_payload: audit_payload
358
+ )
359
+
360
+ expect(decision1).to eq(decision2)
361
+ end
362
+
363
+ it "returns false for different explanations" do
364
+ decision1 = described_class.new(
365
+ decision: "approve",
366
+ confidence: 0.8,
367
+ explanations: ["explanation1"],
368
+ evaluations: [evaluation],
369
+ audit_payload: audit_payload
370
+ )
371
+
372
+ decision2 = described_class.new(
373
+ decision: "approve",
374
+ confidence: 0.8,
375
+ explanations: ["explanation2"],
376
+ evaluations: [evaluation],
377
+ audit_payload: audit_payload
378
+ )
379
+
380
+ expect(decision1).not_to eq(decision2)
381
+ end
382
+
383
+ it "returns false for different evaluations" do
384
+ eval2 = DecisionAgent::Evaluation.new(
385
+ decision: "reject",
386
+ weight: 0.9,
387
+ reason: "Different reason",
388
+ evaluator_name: "OtherEvaluator"
389
+ )
390
+
391
+ decision1 = described_class.new(
392
+ decision: "approve",
393
+ confidence: 0.8,
394
+ explanations: [],
395
+ evaluations: [evaluation],
396
+ audit_payload: audit_payload
397
+ )
398
+
399
+ decision2 = described_class.new(
400
+ decision: "approve",
401
+ confidence: 0.8,
402
+ explanations: [],
403
+ evaluations: [eval2],
404
+ audit_payload: audit_payload
405
+ )
406
+
407
+ expect(decision1).not_to eq(decision2)
408
+ end
409
+
410
+ it "returns false for non-Decision objects" do
411
+ decision = described_class.new(
412
+ decision: "approve",
413
+ confidence: 0.8,
414
+ explanations: [],
415
+ evaluations: [evaluation],
416
+ audit_payload: audit_payload
417
+ )
418
+
419
+ expect(decision).not_to eq("not a decision")
420
+ expect(decision).not_to eq(nil)
421
+ end
422
+ end
423
+ end