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,481 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent::ABTesting::ABTestingAgent do
4
+ let(:version_manager) { double("VersionManager") }
5
+ let(:ab_test_manager) { double("ABTestManager", version_manager: version_manager) }
6
+ let(:base_evaluator) do
7
+ DecisionAgent::Evaluators::StaticEvaluator.new(
8
+ decision: "approve",
9
+ weight: 0.8,
10
+ reason: "Base evaluator"
11
+ )
12
+ end
13
+
14
+ describe "#initialize" do
15
+ it "initializes with ab_test_manager" do
16
+ agent = described_class.new(ab_test_manager: ab_test_manager)
17
+ expect(agent.ab_test_manager).to eq(ab_test_manager)
18
+ end
19
+
20
+ it "uses version_manager from ab_test_manager if not provided" do
21
+ allow(ab_test_manager).to receive(:version_manager).and_return(version_manager)
22
+ agent = described_class.new(ab_test_manager: ab_test_manager)
23
+ expect(agent.version_manager).to eq(version_manager)
24
+ end
25
+
26
+ it "uses provided version_manager" do
27
+ custom_version_manager = double("CustomVersionManager")
28
+ agent = described_class.new(
29
+ ab_test_manager: ab_test_manager,
30
+ version_manager: custom_version_manager
31
+ )
32
+ expect(agent.version_manager).to eq(custom_version_manager)
33
+ end
34
+ end
35
+
36
+ describe "#decide" do
37
+ context "without A/B test" do
38
+ it "makes standard decision" do
39
+ agent = described_class.new(
40
+ ab_test_manager: ab_test_manager,
41
+ evaluators: [base_evaluator]
42
+ )
43
+
44
+ result = agent.decide(context: { user: "test" })
45
+
46
+ expect(result[:decision]).to eq("approve")
47
+ expect(result[:ab_test]).to be_nil
48
+ end
49
+ end
50
+
51
+ context "with A/B test" do
52
+ let(:assignment) do
53
+ {
54
+ assignment_id: "assign_1",
55
+ variant: "A",
56
+ version_id: "version_1"
57
+ }
58
+ end
59
+
60
+ let(:version) do
61
+ {
62
+ content: {
63
+ version: "1.0",
64
+ ruleset: "test",
65
+ rules: [
66
+ {
67
+ id: "rule_1",
68
+ if: { field: "status", op: "eq", value: "active" },
69
+ then: { decision: "approve", weight: 0.9 }
70
+ }
71
+ ]
72
+ }
73
+ }
74
+ end
75
+
76
+ before do
77
+ allow(ab_test_manager).to receive(:assign_variant).and_return(assignment)
78
+ allow(version_manager).to receive(:get_version).and_return(version)
79
+ allow(ab_test_manager).to receive(:record_decision)
80
+ end
81
+
82
+ it "assigns variant and makes decision" do
83
+ agent = described_class.new(
84
+ ab_test_manager: ab_test_manager,
85
+ version_manager: version_manager
86
+ )
87
+
88
+ result = agent.decide(
89
+ context: { status: "active" },
90
+ ab_test_id: "test_1",
91
+ user_id: "user_1"
92
+ )
93
+
94
+ expect(result[:decision]).to eq("approve")
95
+ expect(result[:ab_test]).not_to be_nil
96
+ expect(result[:ab_test][:test_id]).to eq("test_1")
97
+ expect(result[:ab_test][:variant]).to eq("A")
98
+ end
99
+
100
+ it "raises error if version not found" do
101
+ allow(version_manager).to receive(:get_version).and_return(nil)
102
+ agent = described_class.new(
103
+ ab_test_manager: ab_test_manager,
104
+ version_manager: version_manager
105
+ )
106
+
107
+ expect do
108
+ agent.decide(
109
+ context: { status: "active" },
110
+ ab_test_id: "test_1"
111
+ )
112
+ end.to raise_error(DecisionAgent::ABTesting::VersionNotFoundError)
113
+ end
114
+
115
+ it "records decision result" do
116
+ agent = described_class.new(
117
+ ab_test_manager: ab_test_manager,
118
+ version_manager: version_manager
119
+ )
120
+
121
+ agent.decide(
122
+ context: { status: "active" },
123
+ ab_test_id: "test_1"
124
+ )
125
+
126
+ expect(ab_test_manager).to have_received(:record_decision).with(
127
+ assignment_id: "assign_1",
128
+ decision: "approve",
129
+ confidence: be_a(Numeric)
130
+ )
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "#get_test_results" do
136
+ it "delegates to ab_test_manager" do
137
+ agent = described_class.new(ab_test_manager: ab_test_manager)
138
+ allow(ab_test_manager).to receive(:get_results).and_return({ results: [] })
139
+
140
+ result = agent.get_test_results("test_1")
141
+
142
+ expect(ab_test_manager).to have_received(:get_results).with("test_1")
143
+ expect(result).to eq({ results: [] })
144
+ end
145
+ end
146
+
147
+ describe "#active_tests" do
148
+ it "delegates to ab_test_manager" do
149
+ agent = described_class.new(ab_test_manager: ab_test_manager)
150
+ allow(ab_test_manager).to receive(:active_tests).and_return([])
151
+
152
+ result = agent.active_tests
153
+
154
+ expect(ab_test_manager).to have_received(:active_tests)
155
+ expect(result).to eq([])
156
+ end
157
+ end
158
+
159
+ describe "#decide with feedback" do
160
+ it "passes feedback to agent" do
161
+ agent = described_class.new(
162
+ ab_test_manager: ab_test_manager,
163
+ evaluators: [base_evaluator]
164
+ )
165
+
166
+ result = agent.decide(context: { user: "test" }, feedback: { rating: 5 })
167
+
168
+ expect(result[:decision]).to eq("approve")
169
+ end
170
+ end
171
+
172
+ describe "#decide with A/B test - evaluator building" do
173
+ let(:assignment) do
174
+ {
175
+ assignment_id: "assign_1",
176
+ variant: "A",
177
+ version_id: "version_1"
178
+ }
179
+ end
180
+
181
+ let(:version_with_rules) do
182
+ {
183
+ content: {
184
+ version: "1.0",
185
+ ruleset: "test",
186
+ rules: [
187
+ {
188
+ id: "rule_1",
189
+ if: { field: "status", op: "eq", value: "active" },
190
+ then: { decision: "approve", weight: 0.9 }
191
+ }
192
+ ]
193
+ }
194
+ }
195
+ end
196
+
197
+ let(:version_with_evaluators) do
198
+ {
199
+ content: {
200
+ evaluators: [
201
+ {
202
+ type: "json_rule",
203
+ rules: {
204
+ version: "1.0",
205
+ ruleset: "test",
206
+ rules: [
207
+ {
208
+ id: "rule_1",
209
+ if: { field: "status", op: "eq", value: "active" },
210
+ then: { decision: "approve", weight: 0.9 }
211
+ }
212
+ ]
213
+ }
214
+ }
215
+ ]
216
+ }
217
+ }
218
+ end
219
+
220
+ let(:version_with_static_evaluator) do
221
+ {
222
+ content: {
223
+ evaluators: [
224
+ {
225
+ type: "static",
226
+ decision: "reject",
227
+ weight: 0.5,
228
+ reason: "Static test"
229
+ }
230
+ ]
231
+ }
232
+ }
233
+ end
234
+
235
+ before do
236
+ allow(ab_test_manager).to receive(:assign_variant).and_return(assignment)
237
+ allow(ab_test_manager).to receive(:record_decision)
238
+ end
239
+
240
+ it "builds JsonRuleEvaluator from version with rules" do
241
+ allow(version_manager).to receive(:get_version).and_return(version_with_rules)
242
+ agent = described_class.new(
243
+ ab_test_manager: ab_test_manager,
244
+ version_manager: version_manager
245
+ )
246
+
247
+ result = agent.decide(
248
+ context: { status: "active" },
249
+ ab_test_id: "test_1"
250
+ )
251
+
252
+ expect(result[:decision]).to eq("approve")
253
+ end
254
+
255
+ it "builds evaluators from version with evaluator config" do
256
+ allow(version_manager).to receive(:get_version).and_return(version_with_evaluators)
257
+ agent = described_class.new(
258
+ ab_test_manager: ab_test_manager,
259
+ version_manager: version_manager
260
+ )
261
+
262
+ result = agent.decide(
263
+ context: { status: "active" },
264
+ ab_test_id: "test_1"
265
+ )
266
+
267
+ expect(result[:decision]).to eq("approve")
268
+ end
269
+
270
+ it "builds StaticEvaluator from version config" do
271
+ allow(version_manager).to receive(:get_version).and_return(version_with_static_evaluator)
272
+ agent = described_class.new(
273
+ ab_test_manager: ab_test_manager,
274
+ version_manager: version_manager
275
+ )
276
+
277
+ result = agent.decide(
278
+ context: { status: "inactive" },
279
+ ab_test_id: "test_1"
280
+ )
281
+
282
+ expect(result[:decision]).to eq("reject")
283
+ end
284
+
285
+ it "falls back to base evaluators when version content is invalid" do
286
+ invalid_version = { content: "invalid" }
287
+ allow(version_manager).to receive(:get_version).and_return(invalid_version)
288
+ agent = described_class.new(
289
+ ab_test_manager: ab_test_manager,
290
+ version_manager: version_manager,
291
+ evaluators: [base_evaluator]
292
+ )
293
+
294
+ result = agent.decide(
295
+ context: { status: "active" },
296
+ ab_test_id: "test_1"
297
+ )
298
+
299
+ expect(result[:decision]).to eq("approve")
300
+ end
301
+
302
+ it "raises error for unknown evaluator type" do
303
+ invalid_evaluator_version = {
304
+ content: {
305
+ evaluators: [
306
+ {
307
+ type: "unknown_type",
308
+ config: {}
309
+ }
310
+ ]
311
+ }
312
+ }
313
+ allow(version_manager).to receive(:get_version).and_return(invalid_evaluator_version)
314
+ agent = described_class.new(
315
+ ab_test_manager: ab_test_manager,
316
+ version_manager: version_manager
317
+ )
318
+
319
+ expect do
320
+ agent.decide(
321
+ context: { status: "active" },
322
+ ab_test_id: "test_1"
323
+ )
324
+ end.to raise_error(/Unknown evaluator type/)
325
+ end
326
+ end
327
+
328
+ describe "#decide with Context object" do
329
+ it "handles Context object instead of hash" do
330
+ agent = described_class.new(
331
+ ab_test_manager: ab_test_manager,
332
+ evaluators: [base_evaluator]
333
+ )
334
+
335
+ context = DecisionAgent::Context.new({ user: "test" })
336
+ result = agent.decide(context: context)
337
+
338
+ expect(result[:decision]).to eq("approve")
339
+ end
340
+ end
341
+
342
+ describe "initialization with optional parameters" do
343
+ it "initializes with scoring_strategy" do
344
+ scoring_strategy = double("ScoringStrategy")
345
+ agent = described_class.new(
346
+ ab_test_manager: ab_test_manager,
347
+ scoring_strategy: scoring_strategy
348
+ )
349
+
350
+ expect(agent.instance_variable_get(:@scoring_strategy)).to eq(scoring_strategy)
351
+ end
352
+
353
+ it "initializes with audit_adapter" do
354
+ audit_adapter = double("AuditAdapter")
355
+ agent = described_class.new(
356
+ ab_test_manager: ab_test_manager,
357
+ audit_adapter: audit_adapter
358
+ )
359
+
360
+ expect(agent.instance_variable_get(:@audit_adapter)).to eq(audit_adapter)
361
+ end
362
+ end
363
+
364
+ describe "#build_agent" do
365
+ it "uses base evaluators when provided evaluators are empty" do
366
+ agent = described_class.new(
367
+ ab_test_manager: ab_test_manager,
368
+ evaluators: [base_evaluator]
369
+ )
370
+
371
+ # Access private method via send
372
+ built_agent = agent.send(:build_agent, [])
373
+ expect(built_agent).to be_a(DecisionAgent::Agent)
374
+ end
375
+
376
+ it "uses provided evaluators when not empty" do
377
+ custom_evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(
378
+ decision: "reject",
379
+ weight: 0.5
380
+ )
381
+ agent = described_class.new(
382
+ ab_test_manager: ab_test_manager,
383
+ evaluators: [base_evaluator]
384
+ )
385
+
386
+ built_agent = agent.send(:build_agent, [custom_evaluator])
387
+ expect(built_agent).to be_a(DecisionAgent::Agent)
388
+ end
389
+ end
390
+
391
+ describe "#build_evaluators_from_version" do
392
+ it "falls back to base evaluators when content is not a hash" do
393
+ invalid_version = { content: "not a hash" }
394
+ allow(version_manager).to receive(:get_version).and_return(invalid_version)
395
+ agent = described_class.new(
396
+ ab_test_manager: ab_test_manager,
397
+ version_manager: version_manager,
398
+ evaluators: [base_evaluator]
399
+ )
400
+
401
+ evaluators = agent.send(:build_evaluators_from_version, invalid_version)
402
+ expect(evaluators).to eq([base_evaluator])
403
+ end
404
+
405
+ it "falls back to base evaluators when content hash has no evaluators or rules" do
406
+ invalid_version = { content: { other_key: "value" } }
407
+ allow(version_manager).to receive(:get_version).and_return(invalid_version)
408
+ agent = described_class.new(
409
+ ab_test_manager: ab_test_manager,
410
+ version_manager: version_manager,
411
+ evaluators: [base_evaluator]
412
+ )
413
+
414
+ evaluators = agent.send(:build_evaluators_from_version, invalid_version)
415
+ expect(evaluators).to eq([base_evaluator])
416
+ end
417
+ end
418
+
419
+ describe "#build_evaluator_from_config" do
420
+ it "builds JsonRuleEvaluator from config" do
421
+ agent = described_class.new(
422
+ ab_test_manager: ab_test_manager,
423
+ version_manager: version_manager
424
+ )
425
+
426
+ config = {
427
+ type: "json_rule",
428
+ rules: {
429
+ version: "1.0",
430
+ ruleset: "test",
431
+ rules: [
432
+ {
433
+ id: "rule_1",
434
+ if: { field: "status", op: "eq", value: "active" },
435
+ then: { decision: "approve", weight: 0.9 }
436
+ }
437
+ ]
438
+ }
439
+ }
440
+
441
+ evaluator = agent.send(:build_evaluator_from_config, config)
442
+ expect(evaluator).to be_a(DecisionAgent::Evaluators::JsonRuleEvaluator)
443
+ end
444
+
445
+ it "builds StaticEvaluator from config with default weight" do
446
+ agent = described_class.new(
447
+ ab_test_manager: ab_test_manager,
448
+ version_manager: version_manager
449
+ )
450
+
451
+ config = {
452
+ type: "static",
453
+ decision: "approve",
454
+ reason: "Test reason"
455
+ # weight not provided, should default to 1.0
456
+ }
457
+
458
+ evaluator = agent.send(:build_evaluator_from_config, config)
459
+ expect(evaluator).to be_a(DecisionAgent::Evaluators::StaticEvaluator)
460
+ expect(evaluator.decision).to eq("approve")
461
+ end
462
+
463
+ it "builds StaticEvaluator from config with custom weight" do
464
+ agent = described_class.new(
465
+ ab_test_manager: ab_test_manager,
466
+ version_manager: version_manager
467
+ )
468
+
469
+ config = {
470
+ type: "static",
471
+ decision: "reject",
472
+ weight: 0.7,
473
+ reason: "Custom weight"
474
+ }
475
+
476
+ evaluator = agent.send(:build_evaluator_from_config, config)
477
+ expect(evaluator).to be_a(DecisionAgent::Evaluators::StaticEvaluator)
478
+ expect(evaluator.decision).to eq("reject")
479
+ end
480
+ end
481
+ end
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/decision_agent/ab_testing/storage/adapter"
3
+
4
+ RSpec.describe DecisionAgent::ABTesting::Storage::Adapter do
5
+ let(:adapter) { described_class.new }
6
+
7
+ describe "#save_test" do
8
+ it "raises NotImplementedError" do
9
+ test = double("ABTest")
10
+ expect { adapter.save_test(test) }.to raise_error(NotImplementedError, /must implement #save_test/)
11
+ end
12
+ end
13
+
14
+ describe "#get_test" do
15
+ it "raises NotImplementedError" do
16
+ expect { adapter.get_test("test_id") }.to raise_error(NotImplementedError, /must implement #get_test/)
17
+ end
18
+ end
19
+
20
+ describe "#update_test" do
21
+ it "raises NotImplementedError" do
22
+ expect { adapter.update_test("test_id", {}) }.to raise_error(NotImplementedError, /must implement #update_test/)
23
+ end
24
+ end
25
+
26
+ describe "#list_tests" do
27
+ it "raises NotImplementedError" do
28
+ expect { adapter.list_tests }.to raise_error(NotImplementedError, /must implement #list_tests/)
29
+ end
30
+
31
+ it "raises NotImplementedError with status filter" do
32
+ expect { adapter.list_tests(status: "active") }.to raise_error(NotImplementedError, /must implement #list_tests/)
33
+ end
34
+
35
+ it "raises NotImplementedError with limit" do
36
+ expect { adapter.list_tests(limit: 10) }.to raise_error(NotImplementedError, /must implement #list_tests/)
37
+ end
38
+ end
39
+
40
+ describe "#save_assignment" do
41
+ it "raises NotImplementedError" do
42
+ assignment = double("ABTestAssignment")
43
+ expect { adapter.save_assignment(assignment) }.to raise_error(NotImplementedError, /must implement #save_assignment/)
44
+ end
45
+ end
46
+
47
+ describe "#update_assignment" do
48
+ it "raises NotImplementedError" do
49
+ expect { adapter.update_assignment("assignment_id", {}) }.to raise_error(NotImplementedError, /must implement #update_assignment/)
50
+ end
51
+ end
52
+
53
+ describe "#get_assignments" do
54
+ it "raises NotImplementedError" do
55
+ expect { adapter.get_assignments("test_id") }.to raise_error(NotImplementedError, /must implement #get_assignments/)
56
+ end
57
+ end
58
+
59
+ describe "#delete_test" do
60
+ it "raises NotImplementedError" do
61
+ expect { adapter.delete_test("test_id") }.to raise_error(NotImplementedError, /must implement #delete_test/)
62
+ end
63
+ end
64
+ end