decision_agent 0.3.0 → 1.0.1

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. data/spec/web_ui_rack_spec.rb +0 -2134
data/spec/context_spec.rb DELETED
@@ -1,127 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe DecisionAgent::Context do
4
- describe "#initialize" do
5
- it "accepts a hash and freezes it" do
6
- context = DecisionAgent::Context.new({ user: "alice" })
7
-
8
- expect(context.to_h).to eq({ user: "alice" })
9
- expect(context.to_h).to be_frozen
10
- end
11
-
12
- it "converts non-hash to empty hash" do
13
- context = DecisionAgent::Context.new("not a hash")
14
-
15
- expect(context.to_h).to eq({})
16
- end
17
-
18
- it "deep freezes nested hashes" do
19
- data = { user: { name: "alice", roles: ["admin"] } }
20
- context = DecisionAgent::Context.new(data)
21
-
22
- expect(context.to_h[:user]).to be_frozen
23
- expect(context.to_h[:user][:roles]).to be_frozen
24
- end
25
-
26
- it "creates a copy before freezing to avoid mutating original data" do
27
- original_data = { user: { name: "alice", roles: ["admin"] } }
28
- original_data_id = original_data.object_id
29
-
30
- context = DecisionAgent::Context.new(original_data)
31
-
32
- # Should create a copy (different object_id) to avoid mutating original
33
- expect(context.to_h.object_id).not_to eq(original_data_id)
34
- expect(context.to_h).to be_frozen
35
- expect(context.to_h[:user]).to be_frozen
36
- # Original data should not be frozen
37
- expect(original_data).not_to be_frozen
38
- end
39
-
40
- it "skips already frozen objects in deep_freeze" do
41
- frozen_data = { user: { name: "alice", roles: ["admin"] } }
42
- frozen_data.freeze
43
- frozen_data[:user].freeze
44
-
45
- context = DecisionAgent::Context.new(frozen_data)
46
-
47
- expect(context.to_h).to be_frozen
48
- expect(context.to_h[:user]).to be_frozen
49
- end
50
-
51
- it "does not freeze hash keys unnecessarily" do
52
- key_symbol = :test_key
53
- key_string = "test_key"
54
- data = {
55
- key_symbol => "value1",
56
- key_string => "value2"
57
- }
58
-
59
- context = DecisionAgent::Context.new(data)
60
-
61
- # Keys should not be frozen (they're typically symbols/strings that don't need freezing)
62
- expect(context.to_h.keys.first).to eq(key_symbol)
63
- expect(context.to_h.keys.last).to eq(key_string)
64
- # Values should be frozen
65
- expect(context.to_h[key_symbol]).to be_frozen
66
- expect(context.to_h[key_string]).to be_frozen
67
- end
68
- end
69
-
70
- describe "#[]" do
71
- it "retrieves values by key" do
72
- context = DecisionAgent::Context.new({ status: "active" })
73
-
74
- expect(context[:status]).to eq("active")
75
- end
76
-
77
- it "returns nil for missing keys" do
78
- context = DecisionAgent::Context.new({})
79
-
80
- expect(context[:missing]).to be_nil
81
- end
82
- end
83
-
84
- describe "#fetch" do
85
- it "retrieves values by key" do
86
- context = DecisionAgent::Context.new({ priority: "high" })
87
-
88
- expect(context.fetch(:priority)).to eq("high")
89
- end
90
-
91
- it "returns default for missing keys" do
92
- context = DecisionAgent::Context.new({})
93
-
94
- expect(context.fetch(:missing, "default")).to eq("default")
95
- end
96
- end
97
-
98
- describe "#key?" do
99
- it "returns true when key exists" do
100
- context = DecisionAgent::Context.new({ user: "alice" })
101
-
102
- expect(context.key?(:user)).to be true
103
- end
104
-
105
- it "returns false when key does not exist" do
106
- context = DecisionAgent::Context.new({})
107
-
108
- expect(context.key?(:user)).to be false
109
- end
110
- end
111
-
112
- describe "#==" do
113
- it "compares contexts by data equality" do
114
- context1 = DecisionAgent::Context.new({ user: "alice" })
115
- context2 = DecisionAgent::Context.new({ user: "alice" })
116
-
117
- expect(context1).to eq(context2)
118
- end
119
-
120
- it "returns false for different data" do
121
- context1 = DecisionAgent::Context.new({ user: "alice" })
122
- context2 = DecisionAgent::Context.new({ user: "bob" })
123
-
124
- expect(context1).not_to eq(context2)
125
- end
126
- end
127
- end
@@ -1,96 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe DecisionAgent do
4
- before do
5
- # Reset permission_checker between tests to avoid leakage
6
- DecisionAgent.permission_checker = nil
7
- end
8
-
9
- describe ".rbac_config" do
10
- it "returns the global RBAC configuration" do
11
- expect(DecisionAgent.rbac_config).to be_a(DecisionAgent::Auth::RbacConfig)
12
- end
13
- end
14
-
15
- describe ".configure_rbac" do
16
- context "with adapter_type and options" do
17
- it "configures RBAC with adapter type" do
18
- result = DecisionAgent.configure_rbac(:default)
19
- expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
20
- expect(DecisionAgent.rbac_config.adapter).to be_a(DecisionAgent::Auth::RbacAdapter)
21
- end
22
-
23
- it "configures RBAC with custom options" do
24
- result = DecisionAgent.configure_rbac(:custom,
25
- can_proc: ->(_user, _permission, _resource) { true },
26
- has_role_proc: ->(_user, _role) { false },
27
- active_proc: ->(_user) { true })
28
- expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
29
- end
30
- end
31
-
32
- context "with block" do
33
- it "yields the config block" do
34
- # Now test setting a custom adapter via block
35
- custom_adapter = DecisionAgent::Auth::DefaultAdapter.new
36
- result = DecisionAgent.configure_rbac do |config|
37
- config.adapter = custom_adapter
38
- end
39
-
40
- expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
41
- expect(DecisionAgent.rbac_config.adapter).to eq(custom_adapter)
42
- end
43
- end
44
-
45
- context "with no arguments" do
46
- it "returns the rbac_config" do
47
- result = DecisionAgent.configure_rbac
48
- expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
49
- end
50
- end
51
- end
52
-
53
- describe ".permission_checker" do
54
- it "returns a PermissionChecker instance" do
55
- checker = DecisionAgent.permission_checker
56
- expect(checker).to be_a(DecisionAgent::Auth::PermissionChecker)
57
- end
58
-
59
- it "creates a new PermissionChecker if not set" do
60
- # Reset permission_checker
61
- DecisionAgent.permission_checker = nil
62
- checker = DecisionAgent.permission_checker
63
- expect(checker).to be_a(DecisionAgent::Auth::PermissionChecker)
64
- end
65
-
66
- it "returns the same instance on subsequent calls" do
67
- checker1 = DecisionAgent.permission_checker
68
- checker2 = DecisionAgent.permission_checker
69
- expect(checker1).to eq(checker2)
70
- end
71
-
72
- it "uses the rbac_config adapter" do
73
- DecisionAgent.configure_rbac(:default)
74
- checker = DecisionAgent.permission_checker
75
- adapter = checker.instance_variable_get(:@adapter)
76
- expect(adapter).to be_a(DecisionAgent::Auth::RbacAdapter)
77
- expect(DecisionAgent.rbac_config.adapter).to be_a(DecisionAgent::Auth::RbacAdapter)
78
- end
79
- end
80
-
81
- describe ".permission_checker=" do
82
- it "sets a custom permission checker" do
83
- custom_checker = double("CustomChecker")
84
- DecisionAgent.permission_checker = custom_checker
85
- expect(DecisionAgent.permission_checker).to eq(custom_checker)
86
- end
87
-
88
- it "overrides the default permission checker" do
89
- original_checker = DecisionAgent.permission_checker
90
- custom_checker = double("CustomChecker")
91
- DecisionAgent.permission_checker = custom_checker
92
- expect(DecisionAgent.permission_checker).not_to eq(original_checker)
93
- expect(DecisionAgent.permission_checker).to eq(custom_checker)
94
- end
95
- end
96
- end
@@ -1,423 +0,0 @@
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