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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
  10. data/lib/decision_agent/agent.rb +5 -3
  11. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  12. data/lib/decision_agent/auth/authenticator.rb +127 -0
  13. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  14. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  15. data/lib/decision_agent/auth/permission.rb +29 -0
  16. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  17. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  18. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  19. data/lib/decision_agent/auth/role.rb +56 -0
  20. data/lib/decision_agent/auth/session.rb +33 -0
  21. data/lib/decision_agent/auth/session_manager.rb +57 -0
  22. data/lib/decision_agent/auth/user.rb +70 -0
  23. data/lib/decision_agent/context.rb +24 -4
  24. data/lib/decision_agent/decision.rb +10 -3
  25. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  26. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  27. data/lib/decision_agent/errors.rb +38 -0
  28. data/lib/decision_agent/evaluation.rb +10 -3
  29. data/lib/decision_agent/evaluation_validator.rb +8 -13
  30. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  31. data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
  32. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
  33. data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
  34. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
  35. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  36. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  37. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  38. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  39. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  40. data/lib/decision_agent/version.rb +10 -1
  41. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  42. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  43. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  44. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  45. data/lib/decision_agent/web/public/app.js +184 -29
  46. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  47. data/lib/decision_agent/web/public/index.html +37 -9
  48. data/lib/decision_agent/web/public/login.html +298 -0
  49. data/lib/decision_agent/web/public/users.html +679 -0
  50. data/lib/decision_agent/web/server.rb +873 -7
  51. data/lib/decision_agent.rb +59 -0
  52. data/lib/generators/decision_agent/install/install_generator.rb +37 -0
  53. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
  54. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
  55. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
  56. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
  57. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
  58. data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
  59. data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
  60. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
  61. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
  62. data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
  63. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  64. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  65. data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
  66. data/spec/ab_testing/ab_test_spec.rb +270 -0
  67. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  68. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  69. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  70. data/spec/advanced_operators_spec.rb +1003 -0
  71. data/spec/agent_spec.rb +40 -0
  72. data/spec/audit_adapters_spec.rb +18 -0
  73. data/spec/auth/access_audit_logger_spec.rb +394 -0
  74. data/spec/auth/authenticator_spec.rb +112 -0
  75. data/spec/auth/password_reset_spec.rb +294 -0
  76. data/spec/auth/permission_checker_spec.rb +207 -0
  77. data/spec/auth/permission_spec.rb +73 -0
  78. data/spec/auth/rbac_adapter_spec.rb +550 -0
  79. data/spec/auth/rbac_config_spec.rb +82 -0
  80. data/spec/auth/role_spec.rb +51 -0
  81. data/spec/auth/session_manager_spec.rb +172 -0
  82. data/spec/auth/session_spec.rb +112 -0
  83. data/spec/auth/user_spec.rb +130 -0
  84. data/spec/context_spec.rb +43 -0
  85. data/spec/decision_agent_spec.rb +96 -0
  86. data/spec/decision_spec.rb +423 -0
  87. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  88. data/spec/evaluation_spec.rb +364 -0
  89. data/spec/evaluation_validator_spec.rb +165 -0
  90. data/spec/examples.txt +1542 -548
  91. data/spec/issue_verification_spec.rb +95 -21
  92. data/spec/monitoring/metrics_collector_spec.rb +221 -3
  93. data/spec/monitoring/monitored_agent_spec.rb +1 -1
  94. data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
  96. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
  98. data/spec/performance_optimizations_spec.rb +486 -0
  99. data/spec/spec_helper.rb +23 -0
  100. data/spec/testing/batch_test_importer_spec.rb +693 -0
  101. data/spec/testing/batch_test_runner_spec.rb +307 -0
  102. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  103. data/spec/testing/test_result_comparator_spec.rb +392 -0
  104. data/spec/testing/test_scenario_spec.rb +113 -0
  105. data/spec/versioning/adapter_spec.rb +156 -0
  106. data/spec/versioning_spec.rb +253 -0
  107. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  108. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  109. data/spec/web_ui_rack_spec.rb +1705 -0
  110. metadata +123 -6
@@ -0,0 +1,392 @@
1
+ require "spec_helper"
2
+ require "tempfile"
3
+
4
+ RSpec.describe DecisionAgent::Testing::TestResultComparator do
5
+ let(:comparator) { DecisionAgent::Testing::TestResultComparator.new }
6
+
7
+ describe "#compare" do
8
+ let(:scenarios) do
9
+ [
10
+ DecisionAgent::Testing::TestScenario.new(
11
+ id: "test_1",
12
+ context: { user_id: 123 },
13
+ expected_decision: "approve",
14
+ expected_confidence: 0.95
15
+ ),
16
+ DecisionAgent::Testing::TestScenario.new(
17
+ id: "test_2",
18
+ context: { user_id: 456 },
19
+ expected_decision: "reject",
20
+ expected_confidence: 0.80
21
+ )
22
+ ]
23
+ end
24
+
25
+ let(:results) do
26
+ [
27
+ DecisionAgent::Testing::TestResult.new(
28
+ scenario_id: "test_1",
29
+ decision: "approve",
30
+ confidence: 0.95
31
+ ),
32
+ DecisionAgent::Testing::TestResult.new(
33
+ scenario_id: "test_2",
34
+ decision: "reject",
35
+ confidence: 0.80
36
+ )
37
+ ]
38
+ end
39
+
40
+ it "compares results with expected outcomes" do
41
+ summary = comparator.compare(results, scenarios)
42
+
43
+ expect(summary[:total]).to eq(2)
44
+ expect(summary[:matches]).to eq(2)
45
+ expect(summary[:mismatches]).to eq(0)
46
+ expect(summary[:accuracy_rate]).to eq(1.0)
47
+ end
48
+
49
+ it "identifies mismatches" do
50
+ mismatched_results = [
51
+ DecisionAgent::Testing::TestResult.new(
52
+ scenario_id: "test_1",
53
+ decision: "reject", # Wrong decision
54
+ confidence: 0.95
55
+ ),
56
+ DecisionAgent::Testing::TestResult.new(
57
+ scenario_id: "test_2",
58
+ decision: "reject",
59
+ confidence: 0.50 # Wrong confidence
60
+ )
61
+ ]
62
+
63
+ summary = comparator.compare(mismatched_results, scenarios)
64
+
65
+ expect(summary[:matches]).to eq(0)
66
+ expect(summary[:mismatches]).to eq(2)
67
+ expect(summary[:accuracy_rate]).to eq(0.0)
68
+ expect(summary[:mismatches_detail].size).to eq(2)
69
+ end
70
+
71
+ it "handles confidence tolerance" do
72
+ comparator_with_tolerance = DecisionAgent::Testing::TestResultComparator.new(
73
+ confidence_tolerance: 0.1
74
+ )
75
+
76
+ results_with_tolerance = [
77
+ DecisionAgent::Testing::TestResult.new(
78
+ scenario_id: "test_1",
79
+ decision: "approve",
80
+ confidence: 0.96 # Within 0.1 tolerance of 0.95
81
+ )
82
+ ]
83
+
84
+ scenarios_single = [scenarios[0]]
85
+ summary = comparator_with_tolerance.compare(results_with_tolerance, scenarios_single)
86
+
87
+ expect(summary[:matches]).to eq(1)
88
+ expect(summary[:confidence_accuracy]).to eq(1.0)
89
+ end
90
+
91
+ it "handles fuzzy matching" do
92
+ comparator_fuzzy = DecisionAgent::Testing::TestResultComparator.new(fuzzy_match: true)
93
+
94
+ scenarios_fuzzy = [
95
+ DecisionAgent::Testing::TestScenario.new(
96
+ id: "test_1",
97
+ context: { user_id: 123 },
98
+ expected_decision: "APPROVE", # Uppercase
99
+ expected_confidence: 0.95
100
+ )
101
+ ]
102
+
103
+ results_fuzzy = [
104
+ DecisionAgent::Testing::TestResult.new(
105
+ scenario_id: "test_1",
106
+ decision: "approve", # Lowercase - should match with fuzzy
107
+ confidence: 0.95
108
+ )
109
+ ]
110
+
111
+ summary = comparator_fuzzy.compare(results_fuzzy, scenarios_fuzzy)
112
+ expect(summary[:matches]).to eq(1)
113
+ end
114
+
115
+ it "handles fuzzy matching with whitespace" do
116
+ comparator_fuzzy = DecisionAgent::Testing::TestResultComparator.new(fuzzy_match: true)
117
+
118
+ scenarios_fuzzy = [
119
+ DecisionAgent::Testing::TestScenario.new(
120
+ id: "test_1",
121
+ context: { user_id: 123 },
122
+ expected_decision: " approve ", # With spaces
123
+ expected_confidence: 0.95
124
+ )
125
+ ]
126
+
127
+ results_fuzzy = [
128
+ DecisionAgent::Testing::TestResult.new(
129
+ scenario_id: "test_1",
130
+ decision: "approve", # Without spaces - should match with fuzzy
131
+ confidence: 0.95
132
+ )
133
+ ]
134
+
135
+ summary = comparator_fuzzy.compare(results_fuzzy, scenarios_fuzzy)
136
+ expect(summary[:matches]).to eq(1)
137
+ end
138
+
139
+ it "handles nil expected confidence" do
140
+ scenarios_nil_conf = [
141
+ DecisionAgent::Testing::TestScenario.new(
142
+ id: "test_1",
143
+ context: { user_id: 123 },
144
+ expected_decision: "approve",
145
+ expected_confidence: nil
146
+ )
147
+ ]
148
+
149
+ results_nil_conf = [
150
+ DecisionAgent::Testing::TestResult.new(
151
+ scenario_id: "test_1",
152
+ decision: "approve",
153
+ confidence: 0.95
154
+ )
155
+ ]
156
+
157
+ summary = comparator.compare(results_nil_conf, scenarios_nil_conf)
158
+ expect(summary[:matches]).to eq(1)
159
+ end
160
+
161
+ it "handles nil actual confidence when expected is present" do
162
+ scenarios_with_conf = [
163
+ DecisionAgent::Testing::TestScenario.new(
164
+ id: "test_1",
165
+ context: { user_id: 123 },
166
+ expected_decision: "approve",
167
+ expected_confidence: 0.95
168
+ )
169
+ ]
170
+
171
+ results_no_conf = [
172
+ DecisionAgent::Testing::TestResult.new(
173
+ scenario_id: "test_1",
174
+ decision: "approve",
175
+ confidence: nil
176
+ )
177
+ ]
178
+
179
+ summary = comparator.compare(results_no_conf, scenarios_with_conf)
180
+ expect(summary[:matches]).to eq(0)
181
+ expect(summary[:mismatches]).to eq(1)
182
+ end
183
+
184
+ it "handles missing results for scenarios" do
185
+ scenarios_missing = [
186
+ DecisionAgent::Testing::TestScenario.new(
187
+ id: "test_1",
188
+ context: { user_id: 123 },
189
+ expected_decision: "approve",
190
+ expected_confidence: 0.95
191
+ ),
192
+ DecisionAgent::Testing::TestScenario.new(
193
+ id: "test_2",
194
+ context: { user_id: 456 },
195
+ expected_decision: "reject",
196
+ expected_confidence: 0.80
197
+ )
198
+ ]
199
+
200
+ # Only provide result for test_1
201
+ results_missing = [
202
+ DecisionAgent::Testing::TestResult.new(
203
+ scenario_id: "test_1",
204
+ decision: "approve",
205
+ confidence: 0.95
206
+ )
207
+ ]
208
+
209
+ summary = comparator.compare(results_missing, scenarios_missing)
210
+ # Should only compare test_1 since test_2 has no result
211
+ expect(summary[:total]).to eq(1)
212
+ end
213
+
214
+ it "handles confidence outside tolerance" do
215
+ comparator_strict = DecisionAgent::Testing::TestResultComparator.new(
216
+ confidence_tolerance: 0.01
217
+ )
218
+
219
+ scenarios_strict = [
220
+ DecisionAgent::Testing::TestScenario.new(
221
+ id: "test_1",
222
+ context: { user_id: 123 },
223
+ expected_decision: "approve",
224
+ expected_confidence: 0.95
225
+ )
226
+ ]
227
+
228
+ results_outside = [
229
+ DecisionAgent::Testing::TestResult.new(
230
+ scenario_id: "test_1",
231
+ decision: "approve",
232
+ confidence: 0.98 # Outside 0.01 tolerance
233
+ )
234
+ ]
235
+
236
+ summary = comparator_strict.compare(results_outside, scenarios_strict)
237
+ expect(summary[:matches]).to eq(0)
238
+ expect(summary[:confidence_accuracy]).to eq(0.0)
239
+ end
240
+
241
+ it "handles missing expected results" do
242
+ scenarios_no_expected = [
243
+ DecisionAgent::Testing::TestScenario.new(
244
+ id: "test_1",
245
+ context: { user_id: 123 }
246
+ # No expected_decision
247
+ )
248
+ ]
249
+
250
+ summary = comparator.compare(results, scenarios_no_expected)
251
+
252
+ # Should not compare scenarios without expected results
253
+ expect(summary[:total]).to eq(0)
254
+ end
255
+
256
+ it "handles failed test results" do
257
+ failed_results = [
258
+ DecisionAgent::Testing::TestResult.new(
259
+ scenario_id: "test_1",
260
+ error: StandardError.new("Test failed")
261
+ )
262
+ ]
263
+
264
+ # Only compare scenarios that have expected results
265
+ scenarios_with_expected = scenarios.select(&:expected_result?)
266
+ summary = comparator.compare(failed_results, scenarios_with_expected)
267
+
268
+ expect(summary[:mismatches]).to eq(1)
269
+ expect(comparator.comparison_results[0].match).to be false
270
+ end
271
+ end
272
+
273
+ describe "#generate_summary" do
274
+ it "returns empty summary when no comparisons" do
275
+ summary = comparator.generate_summary
276
+
277
+ expect(summary[:total]).to eq(0)
278
+ expect(summary[:matches]).to eq(0)
279
+ expect(summary[:accuracy_rate]).to eq(0.0)
280
+ end
281
+ end
282
+
283
+ describe "#export_csv" do
284
+ it "exports comparison results to CSV" do
285
+ scenarios = [
286
+ DecisionAgent::Testing::TestScenario.new(
287
+ id: "test_1",
288
+ context: { user_id: 123 },
289
+ expected_decision: "approve",
290
+ expected_confidence: 0.95
291
+ )
292
+ ]
293
+
294
+ results = [
295
+ DecisionAgent::Testing::TestResult.new(
296
+ scenario_id: "test_1",
297
+ decision: "approve",
298
+ confidence: 0.95
299
+ )
300
+ ]
301
+
302
+ comparator.compare(results, scenarios)
303
+
304
+ file = Tempfile.new(["comparison", ".csv"])
305
+ comparator.export_csv(file.path)
306
+
307
+ content = File.read(file.path)
308
+ expect(content).to include("scenario_id")
309
+ expect(content).to include("test_1")
310
+ expect(content).to include("true") # match
311
+
312
+ file.unlink
313
+ end
314
+ end
315
+
316
+ describe "#export_json" do
317
+ it "exports comparison results to JSON" do
318
+ scenarios = [
319
+ DecisionAgent::Testing::TestScenario.new(
320
+ id: "test_1",
321
+ context: { user_id: 123 },
322
+ expected_decision: "approve",
323
+ expected_confidence: 0.95
324
+ )
325
+ ]
326
+
327
+ results = [
328
+ DecisionAgent::Testing::TestResult.new(
329
+ scenario_id: "test_1",
330
+ decision: "approve",
331
+ confidence: 0.95
332
+ )
333
+ ]
334
+
335
+ comparator.compare(results, scenarios)
336
+
337
+ file = Tempfile.new(["comparison", ".json"])
338
+ comparator.export_json(file.path)
339
+
340
+ content = JSON.parse(File.read(file.path))
341
+ expect(content).to have_key("summary")
342
+ expect(content).to have_key("results")
343
+ expect(content["summary"]["total"]).to eq(1)
344
+
345
+ file.unlink
346
+ end
347
+
348
+ it "handles empty comparison results" do
349
+ file = Tempfile.new(["comparison", ".csv"])
350
+ comparator.export_csv(file.path)
351
+
352
+ content = File.read(file.path)
353
+ expect(content).to include("scenario_id")
354
+
355
+ file.unlink
356
+ end
357
+ end
358
+
359
+ describe "ComparisonResult" do
360
+ let(:comparison_result) do
361
+ DecisionAgent::Testing::ComparisonResult.new(
362
+ scenario_id: "test_1",
363
+ match: true,
364
+ decision_match: true,
365
+ confidence_match: true,
366
+ differences: [],
367
+ actual: { decision: "approve", confidence: 0.95 },
368
+ expected: { decision: "approve", confidence: 0.95 }
369
+ )
370
+ end
371
+
372
+ it "creates a comparison result" do
373
+ expect(comparison_result.scenario_id).to eq("test_1")
374
+ expect(comparison_result.match).to be true
375
+ expect(comparison_result.decision_match).to be true
376
+ expect(comparison_result.confidence_match).to be true
377
+ end
378
+
379
+ it "converts to hash" do
380
+ hash = comparison_result.to_h
381
+
382
+ expect(hash[:scenario_id]).to eq("test_1")
383
+ expect(hash[:match]).to be true
384
+ expect(hash[:actual][:decision]).to eq("approve")
385
+ expect(hash[:expected][:decision]).to eq("approve")
386
+ end
387
+
388
+ it "freezes the comparison result" do
389
+ expect(comparison_result.frozen?).to be true
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,113 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent::Testing::TestScenario do
4
+ describe "#initialize" do
5
+ it "creates a test scenario with required fields" do
6
+ scenario = DecisionAgent::Testing::TestScenario.new(
7
+ id: "test_1",
8
+ context: { user_id: 123, amount: 1000 }
9
+ )
10
+
11
+ expect(scenario.id).to eq("test_1")
12
+ expect(scenario.context).to eq({ user_id: 123, amount: 1000 })
13
+ expect(scenario.expected_decision).to be_nil
14
+ expect(scenario.expected_confidence).to be_nil
15
+ end
16
+
17
+ it "creates a test scenario with expected results" do
18
+ scenario = DecisionAgent::Testing::TestScenario.new(
19
+ id: "test_2",
20
+ context: { user_id: 456 },
21
+ expected_decision: "approve",
22
+ expected_confidence: 0.95
23
+ )
24
+
25
+ expect(scenario.expected_decision).to eq("approve")
26
+ expect(scenario.expected_confidence).to eq(0.95)
27
+ end
28
+
29
+ it "freezes the scenario for immutability" do
30
+ scenario = DecisionAgent::Testing::TestScenario.new(
31
+ id: "test_3",
32
+ context: { key: "value" }
33
+ )
34
+
35
+ expect(scenario.frozen?).to be true
36
+ end
37
+ end
38
+
39
+ describe "#expected_result?" do
40
+ it "returns true when expected_decision is set" do
41
+ scenario = DecisionAgent::Testing::TestScenario.new(
42
+ id: "test_1",
43
+ context: { key: "value" },
44
+ expected_decision: "approve"
45
+ )
46
+
47
+ expect(scenario.expected_result?).to be true
48
+ end
49
+
50
+ it "returns false when expected_decision is nil" do
51
+ scenario = DecisionAgent::Testing::TestScenario.new(
52
+ id: "test_1",
53
+ context: { key: "value" }
54
+ )
55
+
56
+ expect(scenario.expected_result?).to be false
57
+ end
58
+ end
59
+
60
+ describe "#to_h" do
61
+ it "converts scenario to hash" do
62
+ scenario = DecisionAgent::Testing::TestScenario.new(
63
+ id: "test_1",
64
+ context: { user_id: 123 },
65
+ expected_decision: "approve",
66
+ expected_confidence: 0.9,
67
+ metadata: { source: "csv" }
68
+ )
69
+
70
+ hash = scenario.to_h
71
+
72
+ expect(hash).to eq({
73
+ id: "test_1",
74
+ context: { user_id: 123 },
75
+ expected_decision: "approve",
76
+ expected_confidence: 0.9,
77
+ metadata: { source: "csv" }
78
+ })
79
+ end
80
+ end
81
+
82
+ describe "#==" do
83
+ it "returns true for equal scenarios" do
84
+ scenario1 = DecisionAgent::Testing::TestScenario.new(
85
+ id: "test_1",
86
+ context: { user_id: 123 },
87
+ expected_decision: "approve"
88
+ )
89
+
90
+ scenario2 = DecisionAgent::Testing::TestScenario.new(
91
+ id: "test_1",
92
+ context: { user_id: 123 },
93
+ expected_decision: "approve"
94
+ )
95
+
96
+ expect(scenario1).to eq(scenario2)
97
+ end
98
+
99
+ it "returns false for different scenarios" do
100
+ scenario1 = DecisionAgent::Testing::TestScenario.new(
101
+ id: "test_1",
102
+ context: { user_id: 123 }
103
+ )
104
+
105
+ scenario2 = DecisionAgent::Testing::TestScenario.new(
106
+ id: "test_2",
107
+ context: { user_id: 123 }
108
+ )
109
+
110
+ expect(scenario1).not_to eq(scenario2)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,156 @@
1
+ require "spec_helper"
2
+ require "fileutils"
3
+ require "tempfile"
4
+ require "decision_agent/versioning/adapter"
5
+ require "decision_agent/versioning/file_storage_adapter"
6
+
7
+ RSpec.describe DecisionAgent::Versioning::Adapter do
8
+ let(:temp_dir) { Dir.mktmpdir }
9
+ let(:adapter) { DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: temp_dir) }
10
+ let(:rule_id) { "test_rule_001" }
11
+ let(:rule_content) do
12
+ {
13
+ version: "1.0",
14
+ ruleset: "test_ruleset",
15
+ rules: [
16
+ {
17
+ id: "rule_1",
18
+ if: { field: "amount", op: "gt", value: 100 },
19
+ then: { decision: "approve", weight: 0.8, reason: "High value" }
20
+ }
21
+ ]
22
+ }
23
+ end
24
+
25
+ after do
26
+ FileUtils.rm_rf(temp_dir)
27
+ end
28
+
29
+ describe "#compare_versions" do
30
+ it "returns nil when first version doesn't exist" do
31
+ v2 = adapter.create_version(rule_id: rule_id, content: rule_content)
32
+
33
+ comparison = adapter.compare_versions(version_id_1: "nonexistent", version_id_2: v2[:id])
34
+ expect(comparison).to be_nil
35
+ end
36
+
37
+ it "returns nil when second version doesn't exist" do
38
+ v1 = adapter.create_version(rule_id: rule_id, content: rule_content)
39
+
40
+ comparison = adapter.compare_versions(version_id_1: v1[:id], version_id_2: "nonexistent")
41
+ expect(comparison).to be_nil
42
+ end
43
+
44
+ it "calculates differences correctly with added keys" do
45
+ content1 = { key1: "value1", key2: "value2" }
46
+ content2 = { key1: "value1", key2: "value2", key3: "value3" }
47
+
48
+ v1 = adapter.create_version(rule_id: rule_id, content: content1)
49
+ v2 = adapter.create_version(rule_id: "rule_2", content: content2)
50
+
51
+ comparison = adapter.compare_versions(version_id_1: v1[:id], version_id_2: v2[:id])
52
+
53
+ expect(comparison[:differences][:added]).not_to be_empty
54
+ end
55
+
56
+ it "calculates differences correctly with removed keys" do
57
+ content1 = { key1: "value1", key2: "value2", key3: "value3" }
58
+ content2 = { key1: "value1", key2: "value2" }
59
+
60
+ v1 = adapter.create_version(rule_id: rule_id, content: content1)
61
+ v2 = adapter.create_version(rule_id: "rule_2", content: content2)
62
+
63
+ comparison = adapter.compare_versions(version_id_1: v1[:id], version_id_2: v2[:id])
64
+
65
+ expect(comparison[:differences][:removed]).not_to be_empty
66
+ end
67
+
68
+ it "calculates differences correctly with changed values" do
69
+ content1 = { key1: "value1", key2: "value2" }
70
+ content2 = { key1: "value1", key2: "value2_changed" }
71
+
72
+ v1 = adapter.create_version(rule_id: rule_id, content: content1)
73
+ v2 = adapter.create_version(rule_id: "rule_2", content: content2)
74
+
75
+ comparison = adapter.compare_versions(version_id_1: v1[:id], version_id_2: v2[:id])
76
+
77
+ expect(comparison[:differences][:changed]).to have_key(:key2)
78
+ expect(comparison[:differences][:changed][:key2][:old]).to eq("value2")
79
+ expect(comparison[:differences][:changed][:key2][:new]).to eq("value2_changed")
80
+ end
81
+
82
+ it "does not include nil values in changed differences" do
83
+ content1 = { key1: "value1", key2: "value2" }
84
+ content2 = { key1: "value1", key2: nil }
85
+
86
+ v1 = adapter.create_version(rule_id: rule_id, content: content1)
87
+ v2 = adapter.create_version(rule_id: "rule_2", content: content2)
88
+
89
+ comparison = adapter.compare_versions(version_id_1: v1[:id], version_id_2: v2[:id])
90
+
91
+ # key2 should not be in changed since new value is nil
92
+ expect(comparison[:differences][:changed]).not_to have_key(:key2)
93
+ end
94
+
95
+ it "returns identical versions with empty differences" do
96
+ v1 = adapter.create_version(rule_id: rule_id, content: rule_content)
97
+ v2 = adapter.create_version(rule_id: "rule_2", content: rule_content)
98
+
99
+ comparison = adapter.compare_versions(version_id_1: v1[:id], version_id_2: v2[:id])
100
+
101
+ expect(comparison[:differences][:added]).to be_empty
102
+ expect(comparison[:differences][:removed]).to be_empty
103
+ expect(comparison[:differences][:changed]).to be_empty
104
+ end
105
+ end
106
+
107
+ describe "abstract methods" do
108
+ # Test that abstract methods raise NotImplementedError when called on base class
109
+ # We use a minimal test adapter class for this
110
+ let(:abstract_adapter) do
111
+ Class.new(DecisionAgent::Versioning::Adapter).new
112
+ end
113
+
114
+ it "raises NotImplementedError for create_version" do
115
+ expect do
116
+ abstract_adapter.create_version(rule_id: "test", content: {})
117
+ end.to raise_error(NotImplementedError)
118
+ end
119
+
120
+ it "raises NotImplementedError for list_versions" do
121
+ expect do
122
+ abstract_adapter.list_versions(rule_id: "test")
123
+ end.to raise_error(NotImplementedError)
124
+ end
125
+
126
+ it "raises NotImplementedError for get_version" do
127
+ expect do
128
+ abstract_adapter.get_version(version_id: "test")
129
+ end.to raise_error(NotImplementedError)
130
+ end
131
+
132
+ it "raises NotImplementedError for get_version_by_number" do
133
+ expect do
134
+ abstract_adapter.get_version_by_number(rule_id: "test", version_number: 1)
135
+ end.to raise_error(NotImplementedError)
136
+ end
137
+
138
+ it "raises NotImplementedError for get_active_version" do
139
+ expect do
140
+ abstract_adapter.get_active_version(rule_id: "test")
141
+ end.to raise_error(NotImplementedError)
142
+ end
143
+
144
+ it "raises NotImplementedError for activate_version" do
145
+ expect do
146
+ abstract_adapter.activate_version(version_id: "test")
147
+ end.to raise_error(NotImplementedError)
148
+ end
149
+
150
+ it "raises NotImplementedError for delete_version" do
151
+ expect do
152
+ abstract_adapter.delete_version(version_id: "test")
153
+ end.to raise_error(NotImplementedError)
154
+ end
155
+ end
156
+ end