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.
- checksums.yaml +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "tempfile"
|
|
3
|
-
|
|
4
|
-
RSpec.describe DecisionAgent::Testing::BatchTestImporter do
|
|
5
|
-
let(:importer) { DecisionAgent::Testing::BatchTestImporter.new }
|
|
6
|
-
|
|
7
|
-
describe "#import_csv" do
|
|
8
|
-
context "with valid CSV file" do
|
|
9
|
-
it "imports test scenarios from CSV" do
|
|
10
|
-
csv_content = <<~CSV
|
|
11
|
-
id,user_id,amount,expected_decision,expected_confidence
|
|
12
|
-
test_1,123,1000,approve,0.95
|
|
13
|
-
test_2,456,5000,reject,0.80
|
|
14
|
-
CSV
|
|
15
|
-
|
|
16
|
-
file = Tempfile.new(["test", ".csv"])
|
|
17
|
-
file.write(csv_content)
|
|
18
|
-
file.close
|
|
19
|
-
|
|
20
|
-
scenarios = importer.import_csv(file.path)
|
|
21
|
-
|
|
22
|
-
expect(scenarios.size).to eq(2)
|
|
23
|
-
expect(scenarios[0].id).to eq("test_1")
|
|
24
|
-
expect(scenarios[0].context[:user_id]).to eq("123")
|
|
25
|
-
expect(scenarios[0].context[:amount]).to eq("1000")
|
|
26
|
-
expect(scenarios[0].expected_decision).to eq("approve")
|
|
27
|
-
expect(scenarios[0].expected_confidence).to eq(0.95)
|
|
28
|
-
|
|
29
|
-
file.unlink
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it "handles CSV without expected results" do
|
|
33
|
-
csv_content = <<~CSV
|
|
34
|
-
id,user_id,amount
|
|
35
|
-
test_1,123,1000
|
|
36
|
-
test_2,456,5000
|
|
37
|
-
CSV
|
|
38
|
-
|
|
39
|
-
file = Tempfile.new(["test", ".csv"])
|
|
40
|
-
file.write(csv_content)
|
|
41
|
-
file.close
|
|
42
|
-
|
|
43
|
-
scenarios = importer.import_csv(file.path,
|
|
44
|
-
expected_decision_column: "nonexistent_column",
|
|
45
|
-
expected_confidence_column: "nonexistent_column")
|
|
46
|
-
|
|
47
|
-
expect(scenarios.size).to eq(2)
|
|
48
|
-
expect(scenarios[0].expected_result?).to be false
|
|
49
|
-
|
|
50
|
-
file.unlink
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
it "handles custom column names" do
|
|
54
|
-
csv_content = <<~CSV
|
|
55
|
-
test_id,customer_id,transaction_amount,expected_outcome,expected_score
|
|
56
|
-
test_1,123,1000,approve,0.95
|
|
57
|
-
CSV
|
|
58
|
-
|
|
59
|
-
file = Tempfile.new(["test", ".csv"])
|
|
60
|
-
file.write(csv_content)
|
|
61
|
-
file.close
|
|
62
|
-
|
|
63
|
-
scenarios = importer.import_csv(file.path,
|
|
64
|
-
id_column: "test_id",
|
|
65
|
-
expected_decision_column: "expected_outcome",
|
|
66
|
-
expected_confidence_column: "expected_score")
|
|
67
|
-
|
|
68
|
-
expect(scenarios.size).to eq(1)
|
|
69
|
-
expect(scenarios[0].id).to eq("test_1")
|
|
70
|
-
expect(scenarios[0].context[:customer_id]).to eq("123")
|
|
71
|
-
expect(scenarios[0].context[:transaction_amount]).to eq("1000")
|
|
72
|
-
expect(scenarios[0].expected_decision).to eq("approve")
|
|
73
|
-
expect(scenarios[0].expected_confidence).to eq(0.95)
|
|
74
|
-
|
|
75
|
-
file.unlink
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "handles CSV without header row" do
|
|
79
|
-
csv_content = <<~CSV
|
|
80
|
-
test_1,123,1000
|
|
81
|
-
test_2,456,5000
|
|
82
|
-
CSV
|
|
83
|
-
|
|
84
|
-
file = Tempfile.new(["test", ".csv"])
|
|
85
|
-
file.write(csv_content)
|
|
86
|
-
file.close
|
|
87
|
-
|
|
88
|
-
scenarios = importer.import_csv(file.path, skip_header: false, id_column: "0")
|
|
89
|
-
|
|
90
|
-
# Without headers, column names will be numeric
|
|
91
|
-
expect(scenarios.size).to eq(2)
|
|
92
|
-
|
|
93
|
-
file.unlink
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
it "handles CSV with context_columns option" do
|
|
97
|
-
csv_content = <<~CSV
|
|
98
|
-
id,user_id,amount,extra_field,expected_decision
|
|
99
|
-
test_1,123,1000,extra_value,approve
|
|
100
|
-
CSV
|
|
101
|
-
|
|
102
|
-
file = Tempfile.new(["test", ".csv"])
|
|
103
|
-
file.write(csv_content)
|
|
104
|
-
file.close
|
|
105
|
-
|
|
106
|
-
scenarios = importer.import_csv(file.path, context_columns: %w[user_id amount])
|
|
107
|
-
|
|
108
|
-
expect(scenarios[0].context.keys).to match_array(%i[user_id amount])
|
|
109
|
-
expect(scenarios[0].context[:extra_field]).to be_nil
|
|
110
|
-
|
|
111
|
-
file.unlink
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
it "handles nil context column values" do
|
|
115
|
-
csv_content = <<~CSV
|
|
116
|
-
id,user_id,amount
|
|
117
|
-
test_1,123,
|
|
118
|
-
CSV
|
|
119
|
-
|
|
120
|
-
file = Tempfile.new(["test", ".csv"])
|
|
121
|
-
file.write(csv_content)
|
|
122
|
-
file.close
|
|
123
|
-
|
|
124
|
-
scenarios = importer.import_csv(file.path)
|
|
125
|
-
expect(scenarios.size).to eq(1)
|
|
126
|
-
expect(scenarios[0].context[:amount]).to be_nil
|
|
127
|
-
|
|
128
|
-
file.unlink
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
context "with invalid CSV file" do
|
|
133
|
-
it "raises error when required id column is missing" do
|
|
134
|
-
csv_content = <<~CSV
|
|
135
|
-
user_id,amount
|
|
136
|
-
123,1000
|
|
137
|
-
CSV
|
|
138
|
-
|
|
139
|
-
file = Tempfile.new(["test", ".csv"])
|
|
140
|
-
file.write(csv_content)
|
|
141
|
-
file.close
|
|
142
|
-
|
|
143
|
-
expect do
|
|
144
|
-
importer.import_csv(file.path)
|
|
145
|
-
end.to raise_error(DecisionAgent::ImportError)
|
|
146
|
-
|
|
147
|
-
file.unlink
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
it "collects errors for invalid rows" do
|
|
151
|
-
csv_content = <<~CSV
|
|
152
|
-
id,user_id,amount
|
|
153
|
-
test_1,123,1000
|
|
154
|
-
,456,5000
|
|
155
|
-
test_3,789,2000
|
|
156
|
-
CSV
|
|
157
|
-
|
|
158
|
-
file = Tempfile.new(["test", ".csv"])
|
|
159
|
-
file.write(csv_content)
|
|
160
|
-
file.close
|
|
161
|
-
|
|
162
|
-
scenarios = importer.import_csv(file.path)
|
|
163
|
-
|
|
164
|
-
# Should import valid rows and collect errors
|
|
165
|
-
expect(scenarios.size).to eq(2)
|
|
166
|
-
expect(importer.errors).not_to be_empty
|
|
167
|
-
|
|
168
|
-
file.unlink
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
describe "#import_from_array" do
|
|
174
|
-
it "imports test scenarios from array of hashes" do
|
|
175
|
-
data = [
|
|
176
|
-
{
|
|
177
|
-
id: "test_1",
|
|
178
|
-
user_id: 123,
|
|
179
|
-
amount: 1000,
|
|
180
|
-
expected_decision: "approve",
|
|
181
|
-
expected_confidence: 0.95
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
id: "test_2",
|
|
185
|
-
user_id: 456,
|
|
186
|
-
amount: 5000,
|
|
187
|
-
expected_decision: "reject"
|
|
188
|
-
}
|
|
189
|
-
]
|
|
190
|
-
|
|
191
|
-
scenarios = importer.import_from_array(data)
|
|
192
|
-
|
|
193
|
-
expect(scenarios.size).to eq(2)
|
|
194
|
-
expect(scenarios[0].id).to eq("test_1")
|
|
195
|
-
expect(scenarios[0].context[:user_id]).to eq(123)
|
|
196
|
-
expect(scenarios[0].context[:amount]).to eq(1000)
|
|
197
|
-
expect(scenarios[0].expected_decision).to eq("approve")
|
|
198
|
-
expect(scenarios[0].expected_confidence).to eq(0.95)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
it "handles string keys in hash" do
|
|
202
|
-
data = [
|
|
203
|
-
{
|
|
204
|
-
"id" => "test_1",
|
|
205
|
-
"user_id" => 123,
|
|
206
|
-
"amount" => 1000
|
|
207
|
-
}
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
scenarios = importer.import_from_array(data)
|
|
211
|
-
|
|
212
|
-
expect(scenarios.size).to eq(1)
|
|
213
|
-
expect(scenarios[0].context[:user_id]).to eq(123)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
it "raises ImportError when context is empty and all rows fail" do
|
|
217
|
-
data = [
|
|
218
|
-
{
|
|
219
|
-
id: "test_1",
|
|
220
|
-
expected_decision: "approve"
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
|
|
224
|
-
expect do
|
|
225
|
-
importer.import_from_array(data)
|
|
226
|
-
end.to raise_error(DecisionAgent::ImportError, /Failed to import/)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
it "does not support context_columns option (ignores it)" do
|
|
230
|
-
data = [
|
|
231
|
-
{
|
|
232
|
-
id: "test_1",
|
|
233
|
-
user_id: 123,
|
|
234
|
-
amount: 1000,
|
|
235
|
-
extra_field: "not_ignored",
|
|
236
|
-
expected_decision: "approve"
|
|
237
|
-
}
|
|
238
|
-
]
|
|
239
|
-
|
|
240
|
-
scenarios = importer.import_from_array(data, context_columns: %w[user_id amount])
|
|
241
|
-
# parse_hash_row doesn't use context_columns, so extra_field should still be included
|
|
242
|
-
expect(scenarios[0].context.keys).to include(:user_id, :amount, :extra_field)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
describe "progress callback" do
|
|
247
|
-
it "calls progress callback during CSV import" do
|
|
248
|
-
csv_content = <<~CSV
|
|
249
|
-
id,user_id,amount
|
|
250
|
-
test_1,123,1000
|
|
251
|
-
test_2,456,5000
|
|
252
|
-
test_3,789,2000
|
|
253
|
-
CSV
|
|
254
|
-
|
|
255
|
-
file = Tempfile.new(["test", ".csv"])
|
|
256
|
-
file.write(csv_content)
|
|
257
|
-
file.close
|
|
258
|
-
|
|
259
|
-
progress_calls = []
|
|
260
|
-
importer.import_csv(file.path, progress_callback: lambda { |progress|
|
|
261
|
-
progress_calls << progress
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
expect(progress_calls.size).to eq(3)
|
|
265
|
-
expect(progress_calls.last[:processed]).to eq(3)
|
|
266
|
-
expect(progress_calls.last[:total]).to eq(3)
|
|
267
|
-
expect(progress_calls.last[:percentage]).to be_between(0, 100)
|
|
268
|
-
|
|
269
|
-
file.unlink
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
describe "#import_excel" do
|
|
274
|
-
context "when Roo is available" do
|
|
275
|
-
before do
|
|
276
|
-
skip "Roo gem not available" unless defined?(Roo)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
it "raises error for invalid Excel file" do
|
|
280
|
-
file = Tempfile.new(["test", ".xlsx"])
|
|
281
|
-
file.write("not excel content")
|
|
282
|
-
file.close
|
|
283
|
-
|
|
284
|
-
expect do
|
|
285
|
-
importer.import_excel(file.path)
|
|
286
|
-
end.to raise_error(DecisionAgent::ImportError)
|
|
287
|
-
|
|
288
|
-
file.unlink
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
it "handles HeaderRowNotFoundError" do
|
|
292
|
-
# Mock Roo to raise HeaderRowNotFoundError
|
|
293
|
-
spreadsheet_double = double("Spreadsheet")
|
|
294
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
295
|
-
allow(spreadsheet_double).to receive(:sheets).and_return(["Sheet1"])
|
|
296
|
-
allow(spreadsheet_double).to receive(:default_sheet=)
|
|
297
|
-
allow(spreadsheet_double).to receive(:first_row).and_return(nil)
|
|
298
|
-
allow(spreadsheet_double).to receive(:last_row).and_raise(Roo::HeaderRowNotFoundError)
|
|
299
|
-
|
|
300
|
-
file = Tempfile.new(["test", ".xlsx"])
|
|
301
|
-
file.write("content")
|
|
302
|
-
file.close
|
|
303
|
-
|
|
304
|
-
expect do
|
|
305
|
-
importer.import_excel(file.path)
|
|
306
|
-
end.to raise_error(DecisionAgent::ImportError, /no header row/)
|
|
307
|
-
|
|
308
|
-
file.unlink
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
describe "error handling" do
|
|
314
|
-
it "raises ImportError when all rows fail" do
|
|
315
|
-
csv_content = <<~CSV
|
|
316
|
-
user_id,amount
|
|
317
|
-
123,1000
|
|
318
|
-
CSV
|
|
319
|
-
|
|
320
|
-
file = Tempfile.new(["test", ".csv"])
|
|
321
|
-
file.write(csv_content)
|
|
322
|
-
file.close
|
|
323
|
-
|
|
324
|
-
expect do
|
|
325
|
-
importer.import_csv(file.path)
|
|
326
|
-
end.to raise_error(DecisionAgent::ImportError)
|
|
327
|
-
|
|
328
|
-
file.unlink
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
it "collects warnings" do
|
|
332
|
-
expect(importer.warnings).to eq([])
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
it "handles empty CSV file" do
|
|
336
|
-
file = Tempfile.new(["test", ".csv"])
|
|
337
|
-
file.write("id,user_id\n")
|
|
338
|
-
file.close
|
|
339
|
-
|
|
340
|
-
scenarios = importer.import_csv(file.path)
|
|
341
|
-
expect(scenarios).to be_empty
|
|
342
|
-
|
|
343
|
-
file.unlink
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
describe "edge cases" do
|
|
348
|
-
it "handles numeric confidence values" do
|
|
349
|
-
data = [
|
|
350
|
-
{
|
|
351
|
-
id: "test_1",
|
|
352
|
-
user_id: 123,
|
|
353
|
-
expected_confidence: "0.95"
|
|
354
|
-
}
|
|
355
|
-
]
|
|
356
|
-
|
|
357
|
-
scenarios = importer.import_from_array(data)
|
|
358
|
-
expect(scenarios[0].expected_confidence).to eq(0.95)
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
it "handles empty confidence string" do
|
|
362
|
-
data = [
|
|
363
|
-
{
|
|
364
|
-
id: "test_1",
|
|
365
|
-
user_id: 123,
|
|
366
|
-
expected_confidence: ""
|
|
367
|
-
}
|
|
368
|
-
]
|
|
369
|
-
|
|
370
|
-
scenarios = importer.import_from_array(data)
|
|
371
|
-
# Empty string gets converted to 0.0 by to_f when not stripped empty
|
|
372
|
-
# The code checks !expected_confidence.to_s.strip.empty? before conversion
|
|
373
|
-
expect(scenarios[0].expected_confidence).to eq(0.0)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
it "handles symbol keys in context" do
|
|
377
|
-
data = [
|
|
378
|
-
{
|
|
379
|
-
id: "test_1",
|
|
380
|
-
user_id: 123
|
|
381
|
-
}
|
|
382
|
-
]
|
|
383
|
-
|
|
384
|
-
scenarios = importer.import_from_array(data)
|
|
385
|
-
expect(scenarios[0].context[:user_id]).to eq(123)
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
it "handles numeric confidence in string format" do
|
|
389
|
-
data = [
|
|
390
|
-
{
|
|
391
|
-
id: "test_1",
|
|
392
|
-
user_id: 123,
|
|
393
|
-
expected_confidence: "0.95"
|
|
394
|
-
}
|
|
395
|
-
]
|
|
396
|
-
|
|
397
|
-
scenarios = importer.import_from_array(data)
|
|
398
|
-
expect(scenarios[0].expected_confidence).to eq(0.95)
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
it "handles whitespace in confidence string" do
|
|
402
|
-
data = [
|
|
403
|
-
{
|
|
404
|
-
id: "test_1",
|
|
405
|
-
user_id: 123,
|
|
406
|
-
expected_confidence: " 0.95 "
|
|
407
|
-
}
|
|
408
|
-
]
|
|
409
|
-
|
|
410
|
-
scenarios = importer.import_from_array(data)
|
|
411
|
-
expect(scenarios[0].expected_confidence).to eq(0.95)
|
|
412
|
-
end
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
describe "private methods edge cases" do
|
|
416
|
-
it "handles count_csv_rows errors gracefully" do
|
|
417
|
-
csv_content = <<~CSV
|
|
418
|
-
id,user_id
|
|
419
|
-
test_1,123
|
|
420
|
-
CSV
|
|
421
|
-
|
|
422
|
-
file = Tempfile.new(["test", ".csv"])
|
|
423
|
-
file.write(csv_content)
|
|
424
|
-
file.close
|
|
425
|
-
|
|
426
|
-
# Stub count_csv_rows to raise an error, but allow import_csv to work normally
|
|
427
|
-
allow(importer).to receive(:count_csv_rows).and_raise(StandardError.new("File error"))
|
|
428
|
-
|
|
429
|
-
# Should still work but without progress tracking (total_rows will be nil)
|
|
430
|
-
scenarios = importer.import_csv(file.path, progress_callback: ->(_) {})
|
|
431
|
-
|
|
432
|
-
expect(scenarios.size).to eq(1)
|
|
433
|
-
|
|
434
|
-
file.unlink
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
describe "#import_excel comprehensive tests" do
|
|
439
|
-
context "when Roo is available" do
|
|
440
|
-
before do
|
|
441
|
-
skip "Roo gem not available" unless defined?(Roo)
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
let(:spreadsheet_double) do
|
|
445
|
-
double("Spreadsheet",
|
|
446
|
-
sheets: %w[Sheet1 Sheet2],
|
|
447
|
-
first_row: 1,
|
|
448
|
-
last_row: 3)
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
before do
|
|
452
|
-
allow(spreadsheet_double).to receive(:default_sheet=)
|
|
453
|
-
allow(spreadsheet_double).to receive(:row) do |idx|
|
|
454
|
-
case idx
|
|
455
|
-
when 1
|
|
456
|
-
%w[id user_id amount]
|
|
457
|
-
when 2
|
|
458
|
-
%w[test_1 123 1000]
|
|
459
|
-
when 3
|
|
460
|
-
%w[test_2 456 5000]
|
|
461
|
-
end
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
it "imports Excel file with default sheet" do
|
|
466
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
467
|
-
|
|
468
|
-
scenarios = importer.import_excel("test.xlsx")
|
|
469
|
-
|
|
470
|
-
expect(scenarios.size).to eq(2)
|
|
471
|
-
expect(scenarios[0].id).to eq("test_1")
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
it "imports Excel file with sheet index" do
|
|
475
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
476
|
-
allow(spreadsheet_double).to receive(:sheets).and_return(%w[Sheet1 Sheet2])
|
|
477
|
-
|
|
478
|
-
scenarios = importer.import_excel("test.xlsx", sheet: 1)
|
|
479
|
-
|
|
480
|
-
expect(scenarios.size).to eq(2)
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
it "imports Excel file with sheet name" do
|
|
484
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
485
|
-
|
|
486
|
-
scenarios = importer.import_excel("test.xlsx", sheet: "Sheet2")
|
|
487
|
-
|
|
488
|
-
expect(scenarios.size).to eq(2)
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
it "imports Excel file without header" do
|
|
492
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
493
|
-
allow(spreadsheet_double).to receive(:first_row).and_return(1)
|
|
494
|
-
allow(spreadsheet_double).to receive(:last_row).and_return(2)
|
|
495
|
-
allow(spreadsheet_double).to receive(:row) do |idx|
|
|
496
|
-
case idx
|
|
497
|
-
when 1
|
|
498
|
-
%w[test_1 123 1000]
|
|
499
|
-
when 2
|
|
500
|
-
%w[test_2 456 5000]
|
|
501
|
-
end
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
scenarios = importer.import_excel("test.xlsx", skip_header: false, id_column: "0")
|
|
505
|
-
|
|
506
|
-
expect(scenarios.size).to eq(2)
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
it "calls progress callback during Excel import" do
|
|
510
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
511
|
-
|
|
512
|
-
progress_calls = []
|
|
513
|
-
importer.import_excel("test.xlsx", progress_callback: lambda { |progress|
|
|
514
|
-
progress_calls << progress
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
expect(progress_calls.size).to eq(2)
|
|
518
|
-
expect(progress_calls.last[:processed]).to eq(2)
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
it "handles Excel file with no rows" do
|
|
522
|
-
empty_spreadsheet = double("Spreadsheet",
|
|
523
|
-
sheets: ["Sheet1"],
|
|
524
|
-
first_row: 1,
|
|
525
|
-
last_row: 1,
|
|
526
|
-
row: %w[id user_id amount])
|
|
527
|
-
allow(empty_spreadsheet).to receive(:default_sheet=)
|
|
528
|
-
|
|
529
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(empty_spreadsheet)
|
|
530
|
-
|
|
531
|
-
scenarios = importer.import_excel("test.xlsx")
|
|
532
|
-
expect(scenarios).to be_empty
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
it "handles Excel import with custom column names" do
|
|
536
|
-
allow(Roo::Spreadsheet).to receive(:open).and_return(spreadsheet_double)
|
|
537
|
-
|
|
538
|
-
scenarios = importer.import_excel("test.xlsx",
|
|
539
|
-
id_column: "id",
|
|
540
|
-
expected_decision_column: "expected_decision",
|
|
541
|
-
expected_confidence_column: "expected_confidence")
|
|
542
|
-
|
|
543
|
-
expect(scenarios.size).to eq(2)
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
it "handles StandardError during Excel import" do
|
|
547
|
-
allow(Roo::Spreadsheet).to receive(:open).and_raise(StandardError.new("File corrupted"))
|
|
548
|
-
|
|
549
|
-
expect do
|
|
550
|
-
importer.import_excel("test.xlsx")
|
|
551
|
-
end.to raise_error(DecisionAgent::ImportError, /Failed to read Excel file/)
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
describe "CSV import edge cases" do
|
|
557
|
-
it "handles CSV import without progress callback" do
|
|
558
|
-
csv_content = <<~CSV
|
|
559
|
-
id,user_id,amount
|
|
560
|
-
test_1,123,1000
|
|
561
|
-
CSV
|
|
562
|
-
|
|
563
|
-
file = Tempfile.new(["test", ".csv"])
|
|
564
|
-
file.write(csv_content)
|
|
565
|
-
file.close
|
|
566
|
-
|
|
567
|
-
scenarios = importer.import_csv(file.path, progress_callback: nil)
|
|
568
|
-
expect(scenarios.size).to eq(1)
|
|
569
|
-
|
|
570
|
-
file.unlink
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
it "handles CSV with progress callback but count_csv_rows fails" do
|
|
574
|
-
csv_content = <<~CSV
|
|
575
|
-
id,user_id,amount
|
|
576
|
-
test_1,123,1000
|
|
577
|
-
test_2,456,5000
|
|
578
|
-
CSV
|
|
579
|
-
|
|
580
|
-
file = Tempfile.new(["test", ".csv"])
|
|
581
|
-
file.write(csv_content)
|
|
582
|
-
file.close
|
|
583
|
-
|
|
584
|
-
# Stub count_csv_rows to return nil (simulating error)
|
|
585
|
-
allow(importer).to receive(:count_csv_rows).and_return(nil)
|
|
586
|
-
|
|
587
|
-
progress_calls = []
|
|
588
|
-
scenarios = importer.import_csv(file.path, progress_callback: lambda { |progress|
|
|
589
|
-
progress_calls << progress
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
expect(scenarios.size).to eq(2)
|
|
593
|
-
# Progress callback should not be called when total_rows is nil
|
|
594
|
-
expect(progress_calls).to be_empty
|
|
595
|
-
|
|
596
|
-
file.unlink
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
it "handles CSV row parsing that returns nil scenario" do
|
|
600
|
-
csv_content = <<~CSV
|
|
601
|
-
id,user_id,amount
|
|
602
|
-
test_1,123,1000
|
|
603
|
-
CSV
|
|
604
|
-
|
|
605
|
-
file = Tempfile.new(["test", ".csv"])
|
|
606
|
-
file.write(csv_content)
|
|
607
|
-
file.close
|
|
608
|
-
|
|
609
|
-
# Stub parse_csv_row to return nil
|
|
610
|
-
allow(importer).to receive(:parse_csv_row).and_return(nil)
|
|
611
|
-
|
|
612
|
-
scenarios = importer.import_csv(file.path)
|
|
613
|
-
expect(scenarios).to be_empty
|
|
614
|
-
|
|
615
|
-
file.unlink
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
it "handles extract_value with symbol keys" do
|
|
619
|
-
data = [
|
|
620
|
-
{
|
|
621
|
-
id: "test_1",
|
|
622
|
-
user_id: 123
|
|
623
|
-
}
|
|
624
|
-
]
|
|
625
|
-
|
|
626
|
-
scenarios = importer.import_from_array(data)
|
|
627
|
-
expect(scenarios[0].context[:user_id]).to eq(123)
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
it "handles extract_value with string keys" do
|
|
631
|
-
data = [
|
|
632
|
-
{
|
|
633
|
-
"id" => "test_1",
|
|
634
|
-
"user_id" => 123
|
|
635
|
-
}
|
|
636
|
-
]
|
|
637
|
-
|
|
638
|
-
scenarios = importer.import_from_array(data)
|
|
639
|
-
expect(scenarios[0].id).to eq("test_1")
|
|
640
|
-
expect(scenarios[0].context[:user_id]).to eq(123)
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
it "handles CSV with nil column values in context" do
|
|
644
|
-
csv_content = <<~CSV
|
|
645
|
-
id,user_id,amount,description
|
|
646
|
-
test_1,123,1000,
|
|
647
|
-
CSV
|
|
648
|
-
|
|
649
|
-
file = Tempfile.new(["test", ".csv"])
|
|
650
|
-
file.write(csv_content)
|
|
651
|
-
file.close
|
|
652
|
-
|
|
653
|
-
scenarios = importer.import_csv(file.path)
|
|
654
|
-
expect(scenarios[0].context[:description]).to be_nil
|
|
655
|
-
|
|
656
|
-
file.unlink
|
|
657
|
-
end
|
|
658
|
-
|
|
659
|
-
it "handles CSV with numeric keys when no headers" do
|
|
660
|
-
csv_content = <<~CSV
|
|
661
|
-
test_1,123,1000
|
|
662
|
-
test_2,456,5000
|
|
663
|
-
CSV
|
|
664
|
-
|
|
665
|
-
file = Tempfile.new(["test", ".csv"])
|
|
666
|
-
file.write(csv_content)
|
|
667
|
-
file.close
|
|
668
|
-
|
|
669
|
-
scenarios = importer.import_csv(file.path, skip_header: false, id_column: "0")
|
|
670
|
-
expect(scenarios.size).to eq(2)
|
|
671
|
-
expect(scenarios[0].id).to eq("test_1")
|
|
672
|
-
|
|
673
|
-
file.unlink
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
it "handles CSV context_columns with nil values" do
|
|
677
|
-
csv_content = <<~CSV
|
|
678
|
-
id,user_id,amount,extra
|
|
679
|
-
test_1,123,1000,value
|
|
680
|
-
CSV
|
|
681
|
-
|
|
682
|
-
file = Tempfile.new(["test", ".csv"])
|
|
683
|
-
file.write(csv_content)
|
|
684
|
-
file.close
|
|
685
|
-
|
|
686
|
-
scenarios = importer.import_csv(file.path, context_columns: ["user_id", nil, "amount"])
|
|
687
|
-
expect(scenarios[0].context.keys).to include(:user_id, :amount)
|
|
688
|
-
expect(scenarios[0].context.keys).not_to include(nil)
|
|
689
|
-
|
|
690
|
-
file.unlink
|
|
691
|
-
end
|
|
692
|
-
end
|
|
693
|
-
end
|