decision_agent 0.3.0 → 1.1.0

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. 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