decision_agent 0.2.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
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