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