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,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